最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 来手写一个自己的脚手架吧|项目复盘

    正文概述 掘金(夜剑剑)   2021-03-21   976

    前言

    这几天由于项目需求,搭建了好几次开发环境,每次都得重新配置一大堆东西,想着干脆自己搭一个脚手架好了。于是乎这两天自己简单的实现了一个脚手架,主要还是要能够拓展,写个文章来复盘一下。

    搭建脚手架

    接下来我们一步一步的实现脚手架的功能,感受一下整个过程。搭建用到的模块我都会附上链接,并简单介绍功能。

    初始化项目

    首先我们新建一个文件夹,并初始化

    $ npm init -y
    

    这样项目根目录就有了package.json文件了

    创建命令文件

    我们在根目录创建bin文件夹,并添加my-cli.js文件,内容如下:

    #!/usr/bin/env node
    
    // 把lib下的index.js作为入口
    require('../lib/index')
    

    同时,在package.json中添加以下内容:

        "bin": {
            "my-cli": "bin/my-cli.js"
        },
    

    解释一下这两步,首先package.json中的bin内容,能够让我们在命令行调用my-cli xxx时去执行bin文件夹下面的my-cli.js文件。
    另外#!/usr/bin/env node这句话就是去查找PATH中的node,也就是电脑中安装的nodejs来执行这个文件。 接下来我们执行下面的语句:

    $ npm link
    

    这句命令可以把我们的命令语句my-cli绑定到全局去,然后我们就可以直接在命令行调用一下了,我们试试直接调用

    $ my-cli
    

    会发现报错了,提示找不到..lib/index下的模块,因为我们还没创建。我们在根目录创建lib文件夹,并添加index.js文件,在里面输入console.log("Hello my-cli!")
    然后我们再次执行my-cli,发现控制台输出了"Hello my-cli!"

    处理解析指令

    刚刚我们实现了my-cli指令,但是我们需要的是通过my-cli create xxx这种形式的指令来快速创建项目,那么我们就需要用到commander模块来帮助我们处理指令了。
    先安装模块:

    $ npm i commander
    

    然后修改lib/index.js的内容如下:

    // lib/index.js
    const cmd = require('commander')
    
    cmd
        .version(`${require('../package.json').version}`, '-v --version')
        .usage('<command> [options]');
    
    cmd
        .command('create <name>')
        .description('Create new project')
        .action(async(name) => {
            //这里可以拿到传入的参数
            console.log('projectName:',name);
        });
    
    cmd.parse(process.argv);
    

    关于commander的使用方法可以去主页详细看看,这里我们声明了脚手架的版本,以及定义了create命令。<name>可以解析出传入的字符,并作为参数传入action回调中,因此我们现在调用my-cli create demo试试,结果就会输出projectName: demo
    除了create指令,还可以定义许多别的自定义指令,需要自己去拓展了。

    初始化项目

    接下来我们需要在用户输入my-cli create xxx的时候,在当前目录下生成项目xxx,我们来实现一下这个功能。

    为了不让index.js变得太臃肿,我们在lib下新建order文件夹来处理对应的命令,同时在order下创建create.js:

    // lib/order/create.js
    const { initProjectDir } = require("../utils/create");
    module.exports = async function create(projectName) {
      //初始化项目目录
      initProjectDir(projectName);
    };
    

    同时把lib/index.js内容修改一下:

    //lib/index.js
    const cmd = require('commander');
    const create = require('./order/create');
    cmd
        .version(`${require('../package.json').version}`, '-v --version')
        .usage('<command> [options]');
    
    cmd
        .command('create <name>')
        .description('Create new project')
        .action(async(name) => {
            //这里直接执行create命令
            create(name);
        });
    
    cmd.parse(process.argv);
    

    然后我们在lib/utils/create.js中实现initProjectDir

    const { getProjectPath } = require("./common");
    const { exec, cd } = require("shelljs");
    const { existsSync } = require('fs');
    
    function initProjectDir(projectName) {
      // 判断文件是否已经存在
      const file = getProjectPath(projectName);
      // // 验证文件是否已经存在,存在则退出
      if (existsSync(file)) {
        console.log(`${file} 已经存在`);
        process.exit(1);
      }
      exec(`mkdir ${projectName}`);
      cd(projectName);
    }
    
    module.exports = {
      initProjectDir,
    };
    

    这里用到了shelljs模块,用于在命令行执行语句,安装一下:

    $ npm i shelljs
    

    还有一些共用方法,我们在lib/utils/common.js中添加:

    const { resolve } = require('path')
    
    function getProjectPath(projectName) {
        return resolve(process.cwd(), projectName);
    }
    
    module.exports = {
        getProjectPath,
    }
    

    然后当我们执行my-cli create demo时,就会在当前目录创建出demo文件夹

    处理用户交互

    初始化出了项目的文件夹,接下来就是安装用户需要的功能,我们使用Inquirer来处理用户的交互,先安装一下:

    $ npm i inquirer
    

    然后我们在lib/utils/create.js中添加需要的功能(这里不要直接复制粘贴,是相对于之前添加的内容):

    const { prompt } = require('inquirer')
    async function selectFeature() {
        const { feature } = await prompt([{
            name: 'feature',
            type: 'checkbox',
            message: 'Check the features needed for your project',
            choices: [
                { name: 'vite', value: 'vite', checked: true },
                { name: 'typescript', value: 'typescript' },
                { name: 'babel', value: 'babel' },
            ],
        }, ]);
    
        return feature;
    }
    
    module.exports = {
        selectFeature,
    }
    

    typecheckbox表示让用户多选,choices就是用户可选的模块:

    来手写一个自己的脚手架吧|项目复盘
    选择的结果会存入feature中,可以用于下一步按照模块作为基准。

    安装选中的模块

    用户选好了需要的功能,那么接下来我们就需要把选中的功能给他安装到项目里去,这一块我们分两步走

    添加包依赖

    这一步需要把功能用到的npm模块依赖添加到package.json里面去。

    我们在lib下新建一个文件夹feature,里面的文件负责处理对应的模块。以babel为例,我们在feature下新建babel.js,添加下面的内容:

    // lib/feature/babel.js
    
    const { extendPackage} = require('../utils/common');
    module.exports = function(packageJson) {
        mergePackage(packageJson);
    }
    
    function mergePackage(packageJson) {
        const babelConfig = {
            babel: {
                presets: ['@babel/preset-env'],
            },
            dependencies: {
                'core-js': '^3.8.3',
            },
            devDependencies: {
                '@babel/core': '^7.12.13',
                '@babel/preset-env': '^7.12.13',
                'babel-loader': '^8.2.2',
            },
        }
    
        extendPackage(babelConfig, packageJson);
    }
    

    这里预先把需要的babel依赖版本等写好,然后通过extendPackage函数合并到总的package.json里面去。
    我们在lib/utils/common.js中添加extendPackage函数:

    function extendPackage(minor, main) {
        for (let key in minor) {
            if (main[key] === undefined) {
                main[key] = minor[key];
            } else {
                if (Object.prototype.toString.call(minor[key]) === '[object Object]') {
                    extendPackage(minor[key], main[key]);
                } else {
                    main[key] = minor[key];
                }
            }
        }
    };
    module.exports = {
        extendPackage,
    }
    

    这样package.json文件就被改写掉了,至于什么时候传入packageJson我们等下再介绍。

    添加配置文件

    类似babel的功能,用户可能会用babel.config.js来配置文件,那么我们怎么在用户选择了babel功能后,同时给他创建出一个基本配置好了的配置文件呢?
    答案就是用模板文件,我们可以预先创建好默认的babel.config.js文件,在用户选择了babel功能之后,把默认的配置文件复制一份到新创建的目录下面,方便快捷。
    我们在lib下创建template文件夹,下面对应的每一个文件夹都是一个功能的模块。我们创建babel文件夹,然后添加babel.config.js文件:

    module.exports = {
        presets: [
            [
                '@babel/preset-env',
                {
                    loose: true,
                    targets: { node: 'current' }
                }
            ],
        ],
    };
    

    接下来我们要做的就是,把模板文件按照目录结构复制到新的项目里去,我们在lib/utils/common.js添加赋值模板文件的函数:

    const { resolve } = require('path')
    const { writeFile, readdir, stat, readFileSync, mkdirSync } = require('fs');
    
    async function copyTemplate(from, to) {
        stat(from, (err, stat) => {
            //如果是目录,则遍历复制
            if (stat.isDirectory()) {
                readdir(from, (err, paths) => {
                    paths.forEach(path => {
                        //如果是文件夹,则创建
                        if (!/\./.test(path)) {
                            mkdirSync(to + "\\" + path)
                        }
                        copyTemplate(from + "\\" + path, to + "\\" + path)
                    })
                })
            } else {
                //否则直接复制文件
                writeFile(to, readFileSync(from), () => {});
            }
        })
    }
    
    function generateFiles(tempName) {
        const from = resolve(__dirname, `../template/${tempName}`);
        const to = process.cwd();
        copyTemplate(from, to);
    }
    
    
    module.exports = {
        generateFiles
    }
    

    到这里安装模块的准备工作就完成了,我们回到lib/utils/create.js,添加installFeature函数:

    function installFeature(feature, projectName) {
        //根据需要的feature,到文件名对应的路径下加载对应的功能模块
        const featureArr = feature.map(name => require(`../feature/${name}`));
        
        //设置默认的package.json内容
        const packageJson = {
            name: projectName,
            version: '1.0.0',
            dependencies: {},
            devDependencies: {},
        }
        
        //调用对应功能的创建方法
        featureArr.forEach(item => {
            item(packageJson)
        })
    
        return packageJson;
    }
    
    module.exports = {
      installFeature,
    };
    
    

    这里做了一个遍历用户选中的功能,并执行对应功能模块的创建方法的操作,而合并配置文件和复制模板文件的操作在每个功能模块自己的创建方法里:

    // lib/feature/babel.js
    const { extendPackage, generateFiles } = require('../utils/common');
    module.exports = function(packageJson) {
        mergePackage(packageJson);
        generateFiles('babel');
    }
    
    
    function mergePackage(packageJson) {
        const babelConfig = {
            babel: {
                presets: ['@babel/preset-env'],
            },
            dependencies: {
                'core-js': '^3.8.3',
            },
            devDependencies: {
                '@babel/core': '^7.12.13',
                '@babel/preset-env': '^7.12.13',
                'babel-loader': '^8.2.2',
            },
        }
    
        extendPackage(babelConfig, packageJson);
    }
    

    然后我们回到lib/order/create.js,执行一下installFeature

    const { initProjectDir, selectFeature, installFeature,  } = require("../utils/create");
    
    // create 命令
    module.exports = async function create(projectName) {
        // 初始化项目目录
        initProjectDir(projectName);
    
        // 选择需要的功能
        const feature = await selectFeature();
    
        //安装对应的功能
        const package = installFeature(feature, projectName);
    }
    

    生成package.json

    刚刚通过installFeature,我们拿到了合并完所有功能的最终的package.json配置内容,我们写个函数写入到生成的项目中:

    // lib\utils\create.js
    function initPackage(package) {
        writeFileSync(process.cwd() + "/package.json", JSON.stringify(package, null, 4));
    }
    
    module.exports = {
        initPackage,
    }
    
    // lib\order\create.js
    const { initProjectDir, selectFeature, installFeature, initPackage } = require("../utils/create");
    module.exports = async function create(projectName) {
        // 初始化项目目录
        initProjectDir(projectName);
    
        // 选择需要的功能
        const feature = await selectFeature();
    
        //安装对应的功能
        const package = installFeature(feature, projectName);
    
        // 写入package
        initPackage(package);
    }
    

    这样就生成了package.json文件

    安装依赖

    到这一步,基本上的工作都做完了,我们只需要在新项目中执行npm i把包全都装上就好了:

    // lib\utils\create.js
    const { existsSync, writeFileSync } = require('fs');
    function installModule() {
        exec('npm i')
    }
    module.exports = {
        installModule,
    }
    
    // lib\order\create.js
    const { initProjectDir, selectFeature, installFeature, initPackage, installModule } = require("../utils/create");
    module.exports = async function create(projectName) {
        // 初始化项目目录
        initProjectDir(projectName);
    
        // 选择需要的功能
        const feature = await selectFeature();
    
        //安装对应的功能
        const package = installFeature(feature, projectName);
    
        // 写入package
        initPackage(package);
    
        //进入目录并安装modules
        installModule();
    }
    

    拓展

    如果需要添加功能的话,只需要在lib/feature下面添加对应的功能文件,并在lib/template下添加模板文件即可。
    如果需要添加命令,可以在lib/order下添加命令文件即可。 大功告成!

    总结

    本篇主要讲述了如何搭建一个可拓展的脚手架,并通过模板文件的方式来生成需要的功能。由于本人也是第一次搭建脚手架,存在很多不足的地方,比如模板文件其实还可以设置成动态模板的形式,根据用户的输入改变模板的内容。但是总的来说这些都可以在这个脚手架上进行拓展和改进,如果有什么更好的建议也欢迎大家提出,一定虚心改正!

    写在最后

    1. 以上操作源码已放在github.com/AaronY666/m… ,并按照步骤有commit记录
    2. 很感谢你能看到这里,不妨点个赞支持一下,万分感激~!
    3. 以后会更新更多文章和知识点,感兴趣的话可以关注一波~

    参考文章:

    • 搭建自己的 typescript 项目 + 开发自己的脚手架工具 ts-cli
    • 手把手教你写一个脚手架

    本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情


    起源地下载网 » 来手写一个自己的脚手架吧|项目复盘

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元