从零搭建满足权限校验等需求的前端命令行工具(脚手架)

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"项目背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"开发一款内部使用的脚手架,对工程的管理以及后期框架底层版本的更新迭代,可以节省很多资源。基于此目的,以及命令行脚手架的成长过程需要把控、使用脚手架项目需跟进的情况下,我们提出脚手架一期需满足如下能力:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":" 脚手架更新升级能力 ","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"执行开发命令时,自动校验脚手架是否需要升级,保证最新脚手架版本。 ","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":" 脚手架权限校验能力(内部使用) ","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"脚手架仅限内部使用,用户需先获得权限,且项目需要获得证书。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":" 初始化项目模板能力 ","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"开发环境/生产环境构建能力","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面从零开始介绍。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一、脚手架是什么","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"脚手架是为了保证各施工过程顺利而搭设的工作平台,这是百度百科上对脚手架的定义。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端中,我们耳熟能详的脚手架有create-react-app、vue-cli、yeoman等这类通过命令行工具生成的,也有可以直接使用的模板工程,如:html5-boilerplate/react-boilerplate等。从这些脚手架上看它提供的能力,个人理解为两点:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"提供工程的基础架构代码模板能力。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"建设稳健的工作流保证工程运作。","attrs":{}}]}],"attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二、创建npm包","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1、npm包必备基本项","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"package.json  // 包的基本信息\nREADE.md      // 文档\nindex.js      // 入口文件","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2、package.json配置文件详解","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n  \"name\": \"crxcli\",     // 包名\n  \"version\": \"1.0.0\",   // 版本名\n  \"author\": \"\",         // 作者\n  \"description\": \"\",    // 描述\n  \"main\": \"index.js\",   // 入口文件\n  \"scripts\": {          // 声明npm脚本指令:npm run test\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"bin\":{},             // 声明被放到PATH中的可执行文件\n  \"bugs\":{},            // 项目提交issues的地址\n  \"dependencies\": {},    // 生产环境依赖,npm install --save\n  \"devDependencies\": {}, // 开发环境依赖,npm install --save-dev\n  \"engines\": {},         // 指定项目工作环境\n  \"homepage\": \"\",        // 项目官网地址\n  \"peerDependencies\":{}, // 严格约束插件使用的版本\n  \"private\":\"\",          // 配置私有库,为true,npm不会发布\n  \"publishConfig\": \"\",   // 在publish-time使用的配置集合 \n  \"repository\": \"\",      // 代码存放的地址(git)\n  \"license\": \"ISC\"       // 许可证\n  ...\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3、创建可执行脚本文件","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"新建目录结构","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 创建项目\nmkdir crxCli\ncd crxCli\nnpm init // 初始化配置文件\n\n// 新建可执行脚本文件\ncrxCli/\n bin/\n crx.js // 可执行脚本文件","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"配置package.json文件","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n ...\n  \"bin\":{\n    \"crx\":\"bin/crx.js\"\n  }\n ...\n}","attrs":{}}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"项目与npm模块建立连接","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"cd crxCli && npm link // 全局link\n// 或者\ncd crxCli && npm link 模块名(package.json中的name)\n\n// 解除link\nnpm unlink 模块名","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"建立连接后,可以看到在/npm/node_modules路径下创建了一个软链接,此时便可以使用crx指令进行模块调试。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"三、常见开发包","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"commander // 完整的 node.js 命令行解决方案,\nsemver    // npm版本解析器\nchalk     // 添加终端字符串样式\nminimist  // 参数选择解析器\ndotenv    // 可将环境变量从.env文件加载到process.env中\ndotenv-expand // 在dotenv顶部添加变量扩展\ninquirer  // 用于与命令行交互的工具\nrimraf    // 删除文件\ndownload-git-repo // 下载git项目\nfs-extra  // 文件操作","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"四、搭建命令行脚手架","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在我们基于项目背景,设计下整个业务流程,如图:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"流程图设计","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、登录及模板初始化流程图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/92/9279f3a405845176f55e3f973030d431.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、执行开发环境流程图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ad/ad7607d140731815c63ffb0f2f01e2be.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1、命令行设计","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 登录命令行\ncrx login\n// 登出命令\ncrx loginout\n// 初始化项目模板\ncrx init \n// 执行开发环境\ncrx dev\n// 执行生产环境\ncrx build\n// 查看用户信息\ncrx userinfo","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2、目录结构设计","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"crxCli/\n bin/\n crx.js       // 命令行脚本\n crx-init.js     // 项目初始化脚本\n crx-login.js    // 登录脚本\n crx-loginout.js // 登出脚本\n crx-dev.js      // 开发脚本\n crx-build.js    // 生产脚本\n lib/ // 工具库\n access.js // 权限相关\n ...\n ...\n package.json // 配置文件\n READEME.md","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3、命令行脚本实现","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// bin/crx.js \n\n#! /usr/bin/env node \n// 声明脚本执行的环境\n\nconst { option } = require(\"commander\")\nconst program = require(\"commander\")\nconst version = require(\"../package.json\").version\n\nprogram\n // 版本信息\n .version(version,'-v, --version')\n // 帮助信息的首行提示\n .usage(' [options]')\n // 初始化项目的命令\n .command('init [project]','generate project by template')\n // 开发环境\n .command('dev','start dev server')\n // 生产环境\n .command('build','build source')\n // 登录命令\n .command('login', 'user login')\n // 登出命令\n .command('logout', 'user logout')用户\n // 查看当前用户信息\n .command('userinfo', 'who am i')\n .action(function(cmd){\n if(['init','dev','build','login', 'logout',].indexOf(cmd) === -1){\n console.log('unsupported aid-cli command')\n process.exit(1)\n }\n })\n \nprogram.parse(process.argv)","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"4、登录流程","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// bin/crx-login.js 文件\n\n#! /usr/bin/env node\nprocess.env.NODE_ENV = 'production'\n\nconst program = require('commander')\n// 命令行交互工具\nconst inquirer = require('inquirer')\nconst chalk = require('chalk')\nconst { merge } = require('lodash')\nconst { saveAuthorized } = require('../lib/access')\nconst { login, fetchUserProjects } = require('../lib/api')\n\nprogram.option('--outer', 'outer network', false)\nprogram.parse(process.argv)\n// 判断使用登录网络地址: 外网地址 || 内网\nconst outer = program.outer || false\n\ninquirer.prompt([\n {\n type: 'input',\n name: 'username',\n message: '请输入用户名',\n },\n {\n type: 'password',\n name: 'password',\n message: '请输入密码',\n mask: '*',\n },\n]).then(answers => {\n // 命令行交互获取的用户名/密码\n const username = answers.username\n const password = answers.password\n // axios请求登录接口请求用户信息\n login(username, password, outer).then(({user, token}) => {\n let auth = merge({}, user, { token })\n // 获取用户授权项目\n fetchUserProjects(auth.username === 'admin', auth, outer).then(projects => {\n // 保存.authorized 用户授权信息\n saveAuthorized(merge({}, auth, { projects }))\n console.log(chalk.yellow('登录成功'))\n })\n }).catch(err => {\n console.log(chalk.red(err.response ? err.response.data : err))\n })\n})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// lib/access.js 文件\n\nconst { resolve, join } = require('path')\nconst fs = require('fs-extra')\n\nconst confPath = \"***\"\nconst key = \"****\"\nconst iv = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f])\n\n/**\n * AES是一种常用的对称加密算法,加解密都用同一个密钥.\n * 加密结果通常有两种表示方法:hex和base64\n */\n\n// 加密\nfunction aesEncrypt(data) {\n const cipher = createCipher('aes192', key);\n let crypted = cipher.update(data, 'utf8', 'hex');\n crypted += cipher.final('hex');\n return crypted;\n}\n// 解密\nfunction aesDecrypt(encrypted) {\n const decipher = createDecipher('aes192', key);\n let decrypted = decipher.update(encrypted, 'hex', 'utf8');\n decrypted += decipher.final('utf8');\n return decrypted;\n}\n// 保存授权信息文件\nfunction saveAuthorized(data) {\n fs.ensureDirSync(confPath)\n fs.writeFileSync(join(confPath, '.authorized'), \n aesEncrypt(JSON.stringify(data)))\n}\n// 删除授权文件\nfunction deleteAuthorized() {\n fs.removeSync(join(confPath, '.authorized'))\n}\n// 判断授权文件是否存在\nfunction authorizedExists() {\n return fs.existsSync(join(confPath, '.authorized'))\n}\n// 获取授权文件解析信息\nfunction getAuthorized() {\n let ret = fs.readFileSync(join(confPath, '.authorized'), 'utf8')\n return JSON.parse(aesDecrypt(ret))\n}\nconst { resolve, join } = require('path')\nconst fs = require('fs-extra')","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"5、登出","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// bin/crx-loginout.js\n\n#! /usr/bin/env node\n\nprocess.env.NODE_ENV = 'production'\n\nconst chalk = require('chalk')\nconst { deleteAuthorized, authorizedExists } = require('../lib/access')\n\nif (authorizedExists()) {\n deleteAuthorized()\n console.log(chalk.yellow('注销成功'))\n} else {\n console.log(chalk.red('用户未登录'))\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"6、初始化项目模板","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"初始化项目命令行:","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"crx init ","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"代码实现:","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// bin/crx-init.js\n\n#! /usr/bin/env node\n\nconst program = require('commander')\nconst chalk = require('chalk')\nconst path = require('path')\nconst fs = require('fs')\nconst inquirer = require('inquirer')\nconst rm = require('rimraf').sync\n\nconst config = require('../lib/config')\nconst generator = require('../lib/generator')\nconst { authorizedExists, getAuthorized } = require('../lib/access')\n\nprogram.usage('[project]')\nprogram.option('-r, --repo [value]', 'choose specified repo')\nprogram.option('--outer', 'outer network', false)\n\nprogram.on('--help', function() {\n console.log(`\n Examples:\n ${chalk.cyan(' # create a new project by specified template')}\n crx init Todo\n `)\n})\n\nprogram.parse(process.argv)\n\n// 命令行参数不存在的提示\nif (!program.args.length) {\n program.help()\n}\n\nlet projectName = program.args[0]\nlet projectPath = path.resolve(projectName)\n\nif (projectName === '[object Object]') {\n console.log(chalk.red('name required!'))\n program.help()\n process.exit(1)\n}\n\nconst questions = config.questions\n\n// 判断当前目录是否存在同项目名\nif (fs.existsSync(projectPath)) {\n inquirer\n .prompt([\n {\n type: 'confirm',\n name: 'yes',\n message: 'current project directory is not empty,continue?'\n }\n ])\n .then(function(answer) {\n if (answer.yes) {\n // 删除目录\n rm(projectPath)\n // 确定模板问题,下载模板代码\n ask(questions, generator)\n }\n })\n} else {\n ask(questions, generator)\n}\n\nfunction ask(_questions, cb) {\n let choices = _questions[0].choices\n if (authorizedExists()) {\n // 获取授权项目信息\n const auth = getAuthorized()\n let templates = {}\n // 获取授权项目中可供下载模板类型\n auth.projects.forEach((project) => {\n if (Array.isArray(project.templates)) {\n project.templates.forEach((v) => {\n templates[v.name] = v.value\n })\n }\n })\n // 初始化命令行交互问题\n _questions[0].choices = choices = choices\n .map(v => ({\n // 获取gitlab对应的模板代码下载地址\n name: v.name, value: config.template(v.value, program.outer)\n }))\n .... // 其他代码省略\n }\n _questions.push({\n type: 'confirm',\n name: 'yes',\n message: '确定以上问题的答案吗?'\n })\n\n inquirer.prompt(_questions).then(function(answers) {\n if (answers.yes) {\n // 根据应答下载模板代码\n cb(projectName, projectPath, answers, answers.template)\n } else {\n ask(cb)\n }\n })\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下载模板代码示例:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const ora = require('ora')\nconst execSync = require('execa').shellSync\nconst path = require('path')\nconst fs = require('fs-extra')\nconst chalk = require('chalk')\nconst downloadGitRepo = require('download-git-repo')\nconst { saveHistory } = require('./api')\nconst { authorizedExists, getAuthorized } = require('./access')\n\nfunction prepare(projectName, destPath, answers, repo) {\n generator(projectName, destPath, answers, repo)\n}\n\nfunction download(repo, destPath) {\n // eslint-disable-next-line no-undef\n return new Promise(function(resolve, reject) {\n downloadGitRepo(repo, destPath, function(err) {\n if (err) reject(err)\n resolve(true)\n })\n })\n}\n\nfunction writePKG(pkgPath, data) {\n let pkg = fs.readJsonSync(pkgPath)\n pkg = assign({}, pkg, data)\n fs.writeJsonSync(pkgPath, pkg)\n}\n\nfunction generator(projectName, destPath, answers, repo) {\n // 创建目录\n fs.mkdirsSync(destPath)\n let _dest = 'template: ' + answers.template\n\n let spinner = ora(` 下载模板代码: ${_dest} `)\n spinner.start()\n // 下载模板代码\n return download(repo, destPath).then(function() {\n spinner.stop()\n // 合并模板代码的package.json与命令行交互工具的初始化\n writePKG(path.resolve(projectName, 'package.json'), {\n name: projectName,\n version: answers.version,\n description: answers.desc,\n author: answers.author,\n license: answers.license\n })\n\n try {\n // 进入项目,安装项目依赖\n execSync(`cd ${destPath} && npm install`, { stdio: 'inherit' })\n } catch (err) {\n console.log(err)\n process.exit(1)\n }\n if (authorizedExists) {\n const auth = getAuthorized()\n // 更新代码授权信息\n saveHistory(projectName, auth.id, 'init').catch(()=> {})\n }\n var completeMsg = `成功创建项目: '${projectName}'`\n console.log(chalk.yellow(completeMsg))\n }).catch(function(err) {\n console.log(chalk.red(`\\n 无法下载 ${_dest}`), err)\n process.exit(1)\n })\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"7、开发环境流程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此处仅展示部分判断流程代码,关于webpack开发环境构建相关此处不介绍,后续将整理webpack相关文章。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"执行命令行","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"crx dev","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"代码实现","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"检测是否需要升级脚手架相关代码:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let axios = require('axios')\nlet semver = require('semver')\nlet chalk = require('chalk')\nlet inquirer = require(\"inquirer\")\nlet execSync = require('execa').shellSync\nlet platform = require('os').platform()\nlet pkgJSON = require('../package.json')\n\nmodule.exports = function(done, forceUpdate) {\n if (!semver.satisfies(process.version, pkgJSON.engines.node)) {\n console.log(chalk.red(\n '您的 nodeJS 版本必须满足: >=' + pkgJSON.engines.node + '.x'\n ))\n if (platform === 'darwin') {\n console.log(`推荐使用 ${chalk.cyan('https://github.com/creationix/nvm')} 升级和管理 nodeJS 版本`)\n } else if (platform === 'win32') {\n console.log(`推荐前往 ${chalk.cyan('https://nodejs.org/')} 下载 nodeJS 稳定版`)\n }\n process.exit(1)\n }\n\n const cliName = \"发布npm的脚手架名称\"\n axios.get(`https://registry.npm.taobao.org/${cliName}`, {timeout: 8000}).then(function(ret) {\n if (ret.status === 200) {\n let latest = ret.data['dist-tags'].latest\n let local = pkgJSON.version\n if (semver.lt(local, latest)) {\n console.log(chalk.yellow('发现可升级的 aid-cli 新版本.'))\n console.log('最新: ' + chalk.green(latest))\n console.log('当前: ' + chalk.gray(local))\n if (forceUpdate) {\n try {\n execSync(`npm i ${cliName} -g`, { stdio: 'inherit' })\n done()\n } catch (err) {\n console.error(err)\n process.exit(1)\n }\n } else {\n inquirer.prompt([{\n type: \"confirm\",\n name: 'yes',\n message: \"是否立刻升级?\"\n }]).then(function(answer) {\n if (answer.yes) {\n try {\n execSync(`npm i ${cliName} -g`, { stdio: 'inherit' })\n done()\n } catch (err) {\n console.error(err)\n process.exit(1)\n }\n } else {\n done()\n }\n })\n }\n } else {\n done()\n }\n } else {\n done()\n }\n }).catch(function() {\n console.log(chalk.yellow('脚手架更新检测超时'))\n done()\n })\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"证书校验流程相关代码:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function checkAccess(cb, type, outer) {\n let task = type === 'dev' ? '启动开发服务' : '打包项目'\n let spinner = ora('校验项目...')\n spinner.start()\n // 判断项目是否受限\n const isRestricted = checkRestricted() \n if (isRestricted) {\n const path = resolve('package.json')\n if (fs.existsSync(path)) {\n const projectName = fs.readJSONSync(path).name\n // 用户是否登录获取授权\n if (authorizedExists()) {\n let auth = getAuthorized()\n // 当前项目是否获取授权\n let project = find(auth.projects, (v) => v.name === projectName)\n if (project) {\n // 项目证书是否存在\n if (certExists(projectName)) {\n // 检查项目证书是否过期\n checkCert(projectName, () => {\n saveHistory(projectName, auth.id, type, outer)\n spinner.succeed(chalk.yellow(`校验完成,开始${task}`))\n cb()\n }, spinner)\n } else {\n spinner.stop()\n // spinner.fail(chalk.red('受限项目,开发者证书不存在'))\n inquirer.prompt([\n {\n type: 'confirm',\n name: 'yes',\n message: '开发者证书不存在, 是否生成证书?'\n }\n ])\n .then(function(answer) {\n if (answer.yes) {\n spinner.text = chalk.yellow('安装开发者证书...')\n createCert(auth.id, project.id, projectName, os.homedir(), auth, outer).then(ret => {\n saveCert(projectName, ret.data)\n saveHistory(projectName, auth.id, type, outer)\n spinner.succeed(chalk.yellow(`开发者证书安装完成,开始${task}`))\n cb()\n }).catch(err => {\n console.log(chalk.red(err.response ? err.response.data : err))\n process.exit(1)\n })\n } else {\n console.log(chalk.yellow('bye'))\n process.exit(1)\n }\n })\n }\n } else {\n spinner.fail(chalk.red('用户不属于项目 ' + projectName))\n process.exit(1)\n }\n } else {\n spinner.fail(chalk.red('用户未登录'))\n process.exit(1)\n }\n } else {\n spinner.fail(chalk.red('当前项目的 package.json 文件不存在'))\n process.exit(1)\n }\n } else {\n // console.log(chalk.yellow('非受限项目,无须授权'))\n spinner.succeed(chalk.yellow(`校验完成,开始${task}`))\n cb()\n }\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"五、脚手架的发布","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下仅演示个人账户下发布unscoped包的案例。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1、注册npm账户","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://docs.npmjs.com/cli/v6/commands/npm-publish","title":""},"content":[{"type":"text","text":"注册地址","attrs":{}}]}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"全名\n邮箱\n用户名 // 重要,发布scoped包时会用到\n密码","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2、全局安装nrm","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"npm i nrm -g","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"nrm是npm仓库管理的软件,用于npm仓库的快速切换,常见命令:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"nrm   // 查看可用命令\nnrm ls   // 查看已配置的所有仓库\nnrm test  // 测试所有仓库的响应时间\nnrm add    // 新增仓库\nnrm use     // 切换仓库","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3、发布","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"npm publish","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"若报错未登陆:npm ERR! code ENEEDAUTH ,则执行登录","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"npm adduser","attrs":{}}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"若报错仓库地址不对:npm ERR! code ENEEDAUTH,则先执行切换仓库","attrs":{}}]}],"attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"nrm ls // 查看当前仓库地址\nnrm use 仓库名  // 切换仓库\nnpm publish  // 发布","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"六、总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此一款简单的具备权限校验的命令行工具脚手架便完成了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#ffffff","name":"user"}}],"text":"e","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章