likes
comments
collection
share

如何开发一款前端的脚手架[基础篇]

作者站长头像
站长
· 阅读数 82

简介

最近在研究关于前端的一些内部原理实现,这章主要来学习一下如何开发一款前端的脚手架工具为下一篇脚手架的执行过程和原理做铺垫。

概述

为什么我们需要学习脚手架的开发呢? 我个人觉得原因有如下二点:

  1. 在我们现代基于前端框架开发过程中往往会高频的使用到vuecreate-react-appvite等脚手架工具,我们需要了解他的构建,以至于在报错时能够清晰的定位问题。
  2. 脚手架的目的就是摆脱构建工程时的重复的工作尤其在一些比较通用性的工程上(类似一些辅助插件致力于减少重复工作,提升工作效能。我们就可以有更多时间摸鱼.jpg)

核心包的介绍

下面来介绍一些我在开发脚手架使用到的第三方库

  • commander: Commander强大的Node命令解析工具,其可以让我们更加简单的命令行参数
  • root-check: 尝试降级具有root权限的进程的权限,如果失败,则阻止访问权限
  • userhome: 跨操作系统获取用户主目录操作
  • colors: 命令行输出样式
  • semver: 版本号对比工具
  • url-join: 将所有参数加在一起,并将结果标准化
  • npminstall: 使用Npm快速安装
  • inquirer: 常见交互式命令行用户界面的集合。

功能需求

  • 提供脚手架交互面板
  • 类似实现vue create test -force命令交互(具体如何下载模板不在这里实现,因为太多了,直接查看ak-cli源码)

功能实现

前期准备

初始化项目、安装对应的包、创建index.js文件、定义bin属性值

// index.js最顶部添加如下代码
#! /usr/bin/env node

#! /usr/bin/env node是什么意思呢? 就是从环境变量获取到node、并且使用Node运行该文件。等价于在项目根目录执行node index.js命令。 当然我们也可以写成#! /usr/bin/node。这种写法是直接执行/usr/bin目录下的node,这种写法不推荐因为这样子就把node固定位置了。但每个人的node安装目录会有所不同,所以推荐上面的#! /usr/bin/env node写法 PS: 关于环境变量放在之后讨论 如何开发一款前端的脚手架[基础篇]

// package.json
"bin":{
    "akclown-cli":"./index.js"
},

讲解bin时,我们先探讨一下全局安装@vue/cli发生了什么。答案如下:把@vue/cli依赖下载到node_modules下面,解析package.json文件,发现bin下面有一个配置,就会去node的bin目录下创建vue软链接。 因此我们在shell执行vue实际上是执行这个软连接映射的对应地址的文件。 bin: 表示内部命令对应的可执行文件的路径。

没有发正式包之前通过在项目根目录执行npm link将这个包映射到全局的node_modules进行调试 如何开发一款前端的脚手架[基础篇]

推荐:阅读这篇文章,让你更加理解npm。后续有计划研究npm内部运行流程

root账号检测和降级

检查root,如果在root账户下运行时process.geteuid()方法获取的索引是0. 0就是超级管理员

如果当前文件创建者变为root账户,后续会有很多涉及到权限的问题。因为很多操作都可能没有权限。可以通过ll命令查看创建者 如何开发一款前端的脚手架[基础篇] 因此使用root-check会自动尝试降级

async function core() {
    checkRoot();
}

core();

function checkRoot() {
    const rootCheck = require('root-check');
    // $ 尝试降级具有root权限的进程的权限,如果失败,则阻止访问权限
    rootCheck();
}

检查用户主目录

userHome获取到用户目录,通过path-exists判断主目录是否存在

// console.log('userhome: ', userhome());
// userhome:  C:\Users\ak

function checkUserHome() {
    if (!(userHome && pathExists(userHome))) {
        throw new Error(colors.red(`当前登陆用户主目录不存在`));
    }
}

注册命令

akclown-cli create my-app --force命令拆分如下

  1. 主命令:akclown-cli
  2. command: create
  3. 命令params: my-app
  4. 命令的options: --force
const pkg = require('../package.json');
const program = new Command();

function registerCommand() {
  program
    .name(Object.keys(pkg.bin)[0]) //名称
    .usage('<command> [options]')  // 用法声明

  // $ 注册init命令
  program
    .command('create [projectName]')
    .option('-f --force', '是否强制初始化项目')
    .action(createProject);
    
  program.parse(process.argv);
  // $ 参数小于3个不解析,第一个是node 第二个是脚手架命令, 第三个才是option
  // if (process.argv.length < 3) {
  //   program.outputHelp();
  // }
  // $ args不存在前两个参数  -- node\akclown-cli
  if (program.args && program.args.length < 1) {
    program.outputHelp();
  }
}

通过上面代码就实现了如下交互面板。 如何开发一款前端的脚手架[基础篇]

创建项目逻辑编写

  • 编写createProject函数传入到.action作为回到函数,commander在执行回调函数时会给我们提供的createProject注入参数。 PS: 我在开发PAAS编辑器时提供给业务方的方法也经常这么设计。当内部依赖于外部自定义操作,但外部的执行又依赖于内部的一些内置的参数或者方法,就可以通过传入回调方式实现
function createProject(projectName, options) {
    console.log('projectName, options: ', projectName, options);
}

输出的结构,很明显,我们可以拿到文件目录名称以及是否强制替换 如何开发一款前端的脚手架[基础篇]

  • 通过process.cwd()获取当前文件目录并且判断当前文件目录是否为空.开头的隐藏文件node_modules文件应该被忽略
const localPath = process.cwd();

// 判断当前目录是否为空
function isCwdEmpty(localPath) {
    let fileList = fs.readdirSync(localPath);
    // 忽略掉.开头文件 以及 node_modules目录
    fileList = fileList.filter(
        file => !file.startsWith('.') && !['node_modules'].includes(file)
    );
    return !fileList || fileList.length <= 0;
}
  • 判断--force属性是否为true,如果是且当前目录不为空。弹出用户是否清空目录的提示框
    const localPath = process.cwd();
    if (!isCwdEmpty(localPath)) {
    // 1.1 询问是否继续创建
    let ifContinue = false;
    // $ 强制更新 - 不给予提示
    if (!options.force) {
        ifContinue = (
            await inquirer.prompt({
                type: 'confirm',
                name: 'ifContinue',
                default: false,
                message: '当前文件不为空,是否继续创建项目?',
            })
        ).ifContinue;
        if (!ifContinue) {
            return;
        }
    }
    if (ifContinue || options.force) {
        // $ 给用户做二次确认框
        const { confirmDelete } = await inquirer.prompt({
            type: 'confirm',
            name: 'confirmDelete',
            default: false,
            message: '是否确认清空当前目录下的文件?',
        });
        if (confirmDelete) {
            // $ 清空当前目录
            // fse.emptyDirSync(localPath);
        }
    }

效果如下: 如何开发一款前端的脚手架[基础篇]

  • 获取到用户选择的模板'react' | 'babel' | 'vue'
projectTemplate = await inquirer.prompt([
    {
        type: 'list',
        name: 'projectTemplate',
        message: `请选择项目模板`,
        choices: [
            { value: 'react', name: 'react' },
            { value: 'babel', name: 'babel' },
            { value: 'vue', name: 'vue' }
        ],
    }
]);
console.log('projectTemplate: ', projectTemplate);
console.log('projectName: ', projectName);

如何开发一款前端的脚手架[基础篇]

总结:

通过上面的步骤我们就初步实现了,简单的命令行交互。具体如何下载模板以及安装模板依赖这里不再阐述(有了基础在对具体的业务需求进行定制即可)。有兴趣的同学可以查看AK-cli. 当然这些只是就基本的脚手架开发。还有其他更多内容,有用到后续再针对性的查找即可。例如:Node多进程来实现性能的提升,没记错的话npm install就使用了node的多进程ejs模板渲染修改业务组件库的package.json数据结合simple-git做git提交规范

其他

  1. 在日常编码中,如果存在A方法执行完在执行B方法再执行C方法... 的需求,可以使用微任务队列思维。 A->B->C
let chain = Promise.resolve();
chain = chain.then(() => A);
chain = chain.then(() => B);
chain = chain.then(() => C);
...
  1. 推荐: 使用npkil脚手架轻松删除node_modules。尤其项目是多包管理结构的。谁用谁知道,一个字"香"
  2. npm link添加全局包映射,npm unlink取消全局包映射。当然npm link ~添加参数实现包之间的关联
  3. npm pack打包压缩包。当你在本地开发包时你还不想npm publish发布,但是内部业务团队急着需要某个功能,那你可以打包个npm压缩包给他们。 他们使用通过npm install 压缩包的本地路径即可不发布别人也可以使用
  4. 本demo的git仓库
转载自:https://juejin.cn/post/7249904542866915383
评论
请登录