写点什么

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

用户头像
梁龙先森
关注
发布于: 2020 年 11 月 23 日
脚手架 | 从零搭建满足权限校验等需求的前端命令行工具

项目背景

开发一款内部使用的脚手架,对工程的管理以及后期框架底层版本的更新迭代,可以节省很多资源。基于此目的,以及命令行脚手架的成长过程需要把控、使用脚手架项目需跟进的情况下,我们提出脚手架一期需满足如下能力:

  1.  脚手架更新升级能力

执行开发命令时,自动校验脚手架是否需要升级,保证最新脚手架版本。

  1.  脚手架权限校验能力(内部使用)

脚手架仅限内部使用,用户需先获得权限,且项目需要获得证书。

  1. 初始化项目模板能力

  2. 开发环境/生产环境构建能力

下面从零开始介绍。

一、脚手架是什么

脚手架是为了保证各施工过程顺利而搭设的工作平台,这是百度百科上对脚手架的定义。

前端中,我们耳熟能详的脚手架有 create-react-app、vue-cli、yeoman 等这类通过命令行工具生成的,也有可以直接使用的模板工程,如:html5-boilerplate/react-boilerplate 等。从这些脚手架上看它提供的能力,个人理解为两点:

  1. 提供工程的基础架构代码模板能力。

  2. 建设稳健的工作流保证工程运作。

二、创建 npm 包

1、npm 包必备基本项
package.json  // 包的基本信息READE.md      // 文档index.js      // 入口文件
复制代码
2、package.json 配置文件详解
{  "name": "crxcli",     // 包名  "version": "1.0.0",   // 版本名  "author": "",         // 作者  "description": "",    // 描述  "main": "index.js",   // 入口文件  "scripts": {          // 声明npm脚本指令:npm run test    "test": "echo \"Error: no test specified\" && exit 1"  },  "bin":{},             // 声明被放到PATH中的可执行文件  "bugs":{},            // 项目提交issues的地址  "dependencies": {},    // 生产环境依赖,npm install --save  "devDependencies": {}, // 开发环境依赖,npm install --save-dev  "engines": {},         // 指定项目工作环境  "homepage": "",        // 项目官网地址  "peerDependencies":{}, // 严格约束插件使用的版本  "private":"",          // 配置私有库,为true,npm不会发布  "publishConfig": "",   // 在publish-time使用的配置集合   "repository": "",      // 代码存放的地址(git)  "license": "ISC"       // 许可证  ...}
复制代码
3、创建可执行脚本文件
  1. 新建目录结构

// 创建项目mkdir crxClicd crxClinpm init  // 初始化配置文件
// 新建可执行脚本文件crxCli/ bin/ crx.js // 可执行脚本文件
复制代码
  1. 配置 package.json 文件

{  ...  "bin":{    "crx":"bin/crx.js"  }  ...}
复制代码
  1. 项目与 npm 模块建立连接

cd crxCli && npm link // 全局link// 或者cd crxCli && npm link 模块名(package.json中的name)
// 解除linknpm unlink 模块名
复制代码

建立连接后,可以看到在/npm/node_modules 路径下创建了一个软链接,此时便可以使用 crx 指令进行模块调试。

三、常见开发包

commander // 完整的 node.js 命令行解决方案,semver    // npm版本解析器chalk     // 添加终端字符串样式minimist  // 参数选择解析器dotenv    // 可将环境变量从.env文件加载到process.env中dotenv-expand // 在dotenv顶部添加变量扩展inquirer  // 用于与命令行交互的工具rimraf    // 删除文件download-git-repo // 下载git项目fs-extra  // 文件操作
复制代码

四、搭建命令行脚手架

现在我们基于项目背景,设计下整个业务流程,如图:

流程图设计

1、登录及模板初始化流程图:

2、执行开发环境流程图:

1、命令行设计
// 登录命令行crx login// 登出命令crx loginout// 初始化项目模板crx init <project>// 执行开发环境crx dev// 执行生产环境crx build// 查看用户信息crx userinfo
复制代码
2、目录结构设计
crxCli/ bin/  crx.js          // 命令行脚本  crx-init.js     // 项目初始化脚本  crx-login.js    // 登录脚本  crx-loginout.js // 登出脚本  crx-dev.js      // 开发脚本  crx-build.js    // 生产脚本 lib/              // 工具库  access.js       // 权限相关  ... ... package.json      // 配置文件 READEME.md
复制代码
3、命令行脚本实现
// bin/crx.js 
#! /usr/bin/env node // 声明脚本执行的环境
const { option } = require("commander")const program = require("commander")const version = require("../package.json").version
program // 版本信息 .version(version,'-v, --version') // 帮助信息的首行提示 .usage('<command> [options]') // 初始化项目的命令 .command('init [project]','generate project by template') // 开发环境 .command('dev','start dev server') // 生产环境 .command('build','build source') // 登录命令 .command('login', 'user login') // 登出命令 .command('logout', 'user logout')用户 // 查看当前用户信息 .command('userinfo', 'who am i') .action(function(cmd){ if(['init','dev','build','login', 'logout',].indexOf(cmd) === -1){ console.log('unsupported aid-cli command') process.exit(1) } }) program.parse(process.argv)
复制代码
4、登录流程
// bin/crx-login.js 文件
#! /usr/bin/env nodeprocess.env.NODE_ENV = 'production'
const program = require('commander')// 命令行交互工具const inquirer = require('inquirer')const chalk = require('chalk')const { merge } = require('lodash')const { saveAuthorized } = require('../lib/access')const { login, fetchUserProjects } = require('../lib/api')
program.option('--outer', 'outer network', false)program.parse(process.argv)// 判断使用登录网络地址: 外网地址 || 内网const outer = program.outer || false
inquirer.prompt([ { type: 'input', name: 'username', message: '请输入用户名', }, { type: 'password', name: 'password', message: '请输入密码', mask: '*', },]).then(answers => { // 命令行交互获取的用户名/密码 const username = answers.username const password = answers.password // axios请求登录接口请求用户信息 login(username, password, outer).then(({user, token}) => { let auth = merge({}, user, { token }) // 获取用户授权项目 fetchUserProjects(auth.username === 'admin', auth, outer).then(projects => { // 保存.authorized 用户授权信息 saveAuthorized(merge({}, auth, { projects })) console.log(chalk.yellow('登录成功')) }) }).catch(err => { console.log(chalk.red(err.response ? err.response.data : err)) })})
复制代码


// lib/access.js 文件
const { resolve, join } = require('path')const fs = require('fs-extra')
const confPath = "***"const key = "****"const iv = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f])
/** * AES是一种常用的对称加密算法,加解密都用同一个密钥. * 加密结果通常有两种表示方法:hex和base64 */
// 加密function aesEncrypt(data) { const cipher = createCipher('aes192', key); let crypted = cipher.update(data, 'utf8', 'hex'); crypted += cipher.final('hex'); return crypted;}// 解密function aesDecrypt(encrypted) { const decipher = createDecipher('aes192', key); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted;}// 保存授权信息文件function saveAuthorized(data) { fs.ensureDirSync(confPath) fs.writeFileSync(join(confPath, '.authorized'), aesEncrypt(JSON.stringify(data)))}// 删除授权文件function deleteAuthorized() { fs.removeSync(join(confPath, '.authorized'))}// 判断授权文件是否存在function authorizedExists() { return fs.existsSync(join(confPath, '.authorized'))}// 获取授权文件解析信息function getAuthorized() { let ret = fs.readFileSync(join(confPath, '.authorized'), 'utf8') return JSON.parse(aesDecrypt(ret))}const { resolve, join } = require('path')const fs = require('fs-extra')
复制代码
5、登出
// bin/crx-loginout.js
#! /usr/bin/env node
process.env.NODE_ENV = 'production'
const chalk = require('chalk')const { deleteAuthorized, authorizedExists } = require('../lib/access')
if (authorizedExists()) { deleteAuthorized() console.log(chalk.yellow('注销成功'))} else { console.log(chalk.red('用户未登录'))}
复制代码
6、初始化项目模板
  1. 初始化项目命令行:

crx init <projectName>
复制代码
  1. 代码实现:

// bin/crx-init.js
#! /usr/bin/env node
const program = require('commander')const chalk = require('chalk')const path = require('path')const fs = require('fs')const inquirer = require('inquirer')const rm = require('rimraf').sync
const config = require('../lib/config')const generator = require('../lib/generator')const { authorizedExists, getAuthorized } = require('../lib/access')
program.usage('[project]')program.option('-r, --repo [value]', 'choose specified repo')program.option('--outer', 'outer network', false)
program.on('--help', function() { console.log(` Examples: ${chalk.cyan(' # create a new project by specified template')} crx init Todo `)})
program.parse(process.argv)
// 命令行参数不存在的提示if (!program.args.length) { program.help()}
let projectName = program.args[0]let projectPath = path.resolve(projectName)
if (projectName === '[object Object]') { console.log(chalk.red('name required!')) program.help() process.exit(1)}
const questions = config.questions
// 判断当前目录是否存在同项目名if (fs.existsSync(projectPath)) { inquirer .prompt([ { type: 'confirm', name: 'yes', message: 'current project directory is not empty,continue?' } ]) .then(function(answer) { if (answer.yes) { // 删除目录 rm(projectPath) // 确定模板问题,下载模板代码 ask(questions, generator) } })} else { ask(questions, generator)}
function ask(_questions, cb) { let choices = _questions[0].choices if (authorizedExists()) { // 获取授权项目信息 const auth = getAuthorized() let templates = {} // 获取授权项目中可供下载模板类型 auth.projects.forEach((project) => { if (Array.isArray(project.templates)) { project.templates.forEach((v) => { templates[v.name] = v.value }) } }) // 初始化命令行交互问题 _questions[0].choices = choices = choices .map(v => ({ // 获取gitlab对应的模板代码下载地址 name: v.name, value: config.template(v.value, program.outer) })) .... // 其他代码省略 } _questions.push({ type: 'confirm', name: 'yes', message: '确定以上问题的答案吗?' })
inquirer.prompt(_questions).then(function(answers) { if (answers.yes) { // 根据应答下载模板代码 cb(projectName, projectPath, answers, answers.template) } else { ask(cb) } })}
复制代码

下载模板代码示例:

const ora = require('ora')const execSync = require('execa').shellSyncconst path = require('path')const fs = require('fs-extra')const chalk = require('chalk')const downloadGitRepo = require('download-git-repo')const { saveHistory } = require('./api')const { authorizedExists, getAuthorized } = require('./access')
function prepare(projectName, destPath, answers, repo) { generator(projectName, destPath, answers, repo)}
function download(repo, destPath) { // eslint-disable-next-line no-undef return new Promise(function(resolve, reject) { downloadGitRepo(repo, destPath, function(err) { if (err) reject(err) resolve(true) }) })}
function writePKG(pkgPath, data) { let pkg = fs.readJsonSync(pkgPath) pkg = assign({}, pkg, data) fs.writeJsonSync(pkgPath, pkg)}
function generator(projectName, destPath, answers, repo) { // 创建目录 fs.mkdirsSync(destPath) let _dest = 'template: ' + answers.template
let spinner = ora(` 下载模板代码: ${_dest} `) spinner.start() // 下载模板代码 return download(repo, destPath).then(function() { spinner.stop() // 合并模板代码的package.json与命令行交互工具的初始化 writePKG(path.resolve(projectName, 'package.json'), { name: projectName, version: answers.version, description: answers.desc, author: answers.author, license: answers.license })
try { // 进入项目,安装项目依赖 execSync(`cd ${destPath} && npm install`, { stdio: 'inherit' }) } catch (err) { console.log(err) process.exit(1) } if (authorizedExists) { const auth = getAuthorized() // 更新代码授权信息 saveHistory(projectName, auth.id, 'init').catch(()=> {}) } var completeMsg = `成功创建项目: '${projectName}'` console.log(chalk.yellow(completeMsg)) }).catch(function(err) { console.log(chalk.red(`\n 无法下载 ${_dest}`), err) process.exit(1) })}
复制代码
7、开发环境流程

此处仅展示部分判断流程代码,关于 webpack 开发环境构建相关此处不介绍,后续将整理 webpack 相关文章。

  1. 执行命令行

crx dev
复制代码
  1. 代码实现

检测是否需要升级脚手架相关代码:

let axios = require('axios')let semver = require('semver')let chalk = require('chalk')let inquirer = require("inquirer")let execSync = require('execa').shellSynclet platform = require('os').platform()let pkgJSON = require('../package.json')
module.exports = function(done, forceUpdate) { if (!semver.satisfies(process.version, pkgJSON.engines.node)) { console.log(chalk.red( '您的 nodeJS 版本必须满足: >=' + pkgJSON.engines.node + '.x' )) if (platform === 'darwin') { console.log(`推荐使用 ${chalk.cyan('https://github.com/creationix/nvm')} 升级和管理 nodeJS 版本`) } else if (platform === 'win32') { console.log(`推荐前往 ${chalk.cyan('https://nodejs.org/')} 下载 nodeJS 稳定版`) } process.exit(1) }
const cliName = "发布npm的脚手架名称" axios.get(`https://registry.npm.taobao.org/${cliName}`, {timeout: 8000}).then(function(ret) { if (ret.status === 200) { let latest = ret.data['dist-tags'].latest let local = pkgJSON.version if (semver.lt(local, latest)) { console.log(chalk.yellow('发现可升级的 aid-cli 新版本.')) console.log('最新: ' + chalk.green(latest)) console.log('当前: ' + chalk.gray(local)) if (forceUpdate) { try { execSync(`npm i ${cliName} -g`, { stdio: 'inherit' }) done() } catch (err) { console.error(err) process.exit(1) } } else { inquirer.prompt([{ type: "confirm", name: 'yes', message: "是否立刻升级?" }]).then(function(answer) { if (answer.yes) { try { execSync(`npm i ${cliName} -g`, { stdio: 'inherit' }) done() } catch (err) { console.error(err) process.exit(1) } } else { done() } }) } } else { done() } } else { done() } }).catch(function() { console.log(chalk.yellow('脚手架更新检测超时')) done() })}
复制代码

证书校验流程相关代码:

function checkAccess(cb, type, outer) {  let task = type === 'dev' ? '启动开发服务' : '打包项目'  let spinner = ora('校验项目...')  spinner.start()  // 判断项目是否受限  const isRestricted = checkRestricted()   if (isRestricted) {    const path = resolve('package.json')    if (fs.existsSync(path)) {      const projectName = fs.readJSONSync(path).name      // 用户是否登录获取授权      if (authorizedExists()) {        let auth = getAuthorized()        // 当前项目是否获取授权        let project = find(auth.projects, (v) => v.name === projectName)        if (project) {          // 项目证书是否存在          if (certExists(projectName)) {            // 检查项目证书是否过期            checkCert(projectName, () => {              saveHistory(projectName, auth.id, type, outer)              spinner.succeed(chalk.yellow(`校验完成,开始${task}`))              cb()            }, spinner)          } else {            spinner.stop()            // spinner.fail(chalk.red('受限项目,开发者证书不存在'))            inquirer.prompt([              {                type: 'confirm',                name: 'yes',                message: '开发者证书不存在, 是否生成证书?'              }            ])            .then(function(answer) {              if (answer.yes) {                spinner.text = chalk.yellow('安装开发者证书...')                createCert(auth.id, project.id, projectName, os.homedir(), auth, outer).then(ret => {                  saveCert(projectName, ret.data)                  saveHistory(projectName, auth.id, type, outer)                  spinner.succeed(chalk.yellow(`开发者证书安装完成,开始${task}`))                  cb()                }).catch(err => {                  console.log(chalk.red(err.response ? err.response.data : err))                  process.exit(1)                })              } else {                console.log(chalk.yellow('bye'))                process.exit(1)              }            })          }        } else {          spinner.fail(chalk.red('用户不属于项目 ' + projectName))          process.exit(1)        }      } else {        spinner.fail(chalk.red('用户未登录'))        process.exit(1)      }    } else {      spinner.fail(chalk.red('当前项目的 package.json 文件不存在'))      process.exit(1)    }  } else {    // console.log(chalk.yellow('非受限项目,无须授权'))    spinner.succeed(chalk.yellow(`校验完成,开始${task}`))    cb()  }}
复制代码

五、脚手架的发布

以下仅演示个人账户下发布 unscoped 包的案例。

1、注册 npm 账户

注册地址

全名邮箱用户名   // 重要,发布scoped包时会用到密码
复制代码
2、全局安装 nrm
npm i nrm -g
复制代码

nrm 是 npm 仓库管理的软件,用于 npm 仓库的快速切换,常见命令:

nrm   // 查看可用命令nrm ls   // 查看已配置的所有仓库nrm test  // 测试所有仓库的响应时间nrm add <registry> <url>  // 新增仓库nrm use <registry>    // 切换仓库
复制代码
3、发布
npm publish
复制代码
  1. 若报错未登陆:npm ERR! code ENEEDAUTH ,则执行登录

npm adduser
复制代码
  1. 若报错仓库地址不对:npm ERR! code ENEEDAUTH,则先执行切换仓库

nrm ls // 查看当前仓库地址nrm use 仓库名  // 切换仓库npm publish  // 发布
复制代码

六、总结

至此一款简单的具备权限校验的命令行工具脚手架便完成了。


--END--


作者:梁龙先森 WX:newBlob


原创作品,抄袭必究!


e

发布于: 2020 年 11 月 23 日阅读数: 811
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
脚手架 | 从零搭建满足权限校验等需求的前端命令行工具