1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        簡單寫一個(gè)前端腳手架

        共 14088字,需瀏覽 29分鐘

         ·

        2024-05-23 10:39

            

        大廠技術(shù)  高級(jí)前端  Node進(jìn)階

        點(diǎn)擊上方 程序員成長指北,關(guān)注公眾號(hào)

        回復(fù)1,加入高級(jí)Node交流群

        引言

        腳手架是什么,相信各位已經(jīng)熟悉得不能再熟悉了,畢竟無論是vue開發(fā)者(vue-cli)還是react(create-react-app)開發(fā)者,他們都有各自的腳手架,個(gè)人雖是用react更多,但不得不說是更喜歡vue-cli的,它的插件機(jī)制非常有意思,雖不如webpack的plugin那么方便,但也很強(qiáng)大。不過再講這強(qiáng)大的功能之前,原諒我先水一篇腳手架的基礎(chǔ)。

        腳手架會(huì)分兩篇來講,本篇為基礎(chǔ)篇,講一講最簡單的腳手架如何搭建,入個(gè)門。

        正文

        概念與優(yōu)點(diǎn)

        相信很多開發(fā)者都有這么一段經(jīng)歷,那就是在開始新項(xiàng)目之前,先把舊項(xiàng)目拉下來,刪刪減減,只留下初始化項(xiàng)目時(shí)的配置,一切業(yè)務(wù)代碼都刪了,然后再開始新項(xiàng)目的開發(fā)。一次兩次如此做還好,但再多了就很厭煩,特別是刪代碼還很難保證項(xiàng)目的純凈,會(huì)出現(xiàn)漏刪或者刪多了的問題。而這時(shí)候,你就需要一個(gè)腳手架。

        腳手架是什么,他就是一個(gè)純凈的項(xiàng)目,可以完全不包含業(yè)務(wù)代碼,每次開始新項(xiàng)目之前,跑一下腳手架的命令,那么一個(gè)純凈的項(xiàng)目就初始化出來了,可以直接在這之上進(jìn)行開發(fā)。

        無論是公司還是個(gè)人私底下做項(xiàng)目練手,都極其建議寫一個(gè)腳手架,就算是像本文這樣做一個(gè)最簡單的也是好的。

        那么,腳手架該如何做搭建呢,請移步到下文~

        實(shí)現(xiàn)

        前提:所使用到的第三方庫

        • Commander[1] 完整的node命令行解決方。當(dāng)然也可以使用yargs[2],yargs功能更多一些。
        • Chalk[3] 能給shell命令行的文字添加樣式,簡單來說就是拿來畫畫的,可要可不要。
        • fs-extra[4] 操作文件的,比之node自帶的fs,這個(gè)會(huì)更加強(qiáng)大與完善些。
        • inquirer[5] 在shell命令行中提供交互的庫,具體效果看下文的演示。
        • ora[6] 在shell命令行中展示loading效果
        • download-git-repo[7] 下載git倉庫。

        步驟一:指定執(zhí)行的文件

        • 先創(chuàng)建一個(gè)項(xiàng)目 執(zhí)行npm init -y
        • 創(chuàng)建一個(gè)bin文件夾,添加index.js文件,在這個(gè)文件中寫下#! /usr/bin/env node 此時(shí)目錄結(jié)構(gòu)如下:
        • 在package.json中指定執(zhí)行命令和執(zhí)行的文件
        image.png
        • 執(zhí)行 npm link 命令,鏈接到本地環(huán)境中 npm link (只有本地開發(fā)需要執(zhí)行這一步,正常腳手架全局安裝無需執(zhí)行此步驟)Link 相當(dāng)于將當(dāng)前本地模塊鏈接到npm目錄下,這個(gè)目錄可以直接訪問,所以當(dāng)前包就能直接訪問了。默認(rèn)package.json的name為基準(zhǔn),也可以通過bin配置別名。link完后,npm會(huì)自動(dòng)幫忙生成命令,之后可以直接執(zhí)行cli xxx。

        步驟二:配置可執(zhí)行命令

        • 直接在bin/index.js下配置create命令。直接貼代碼了,里面涉及到的都是第三方庫的api,不了解的先查下文檔較好。

        ps:以下代碼都是mjs,所以需要在package.json中添加一行 "type": "module"

        // 1 配置可執(zhí)行的命令 commander
        import { Command } from 'commander';
        import chalk from 'chalk';
        import config from '../package.json' assert { type'json' };

        const program = new Command();

        program
          .command('create <app-name>')  // 創(chuàng)建命令
          .description('create a new project'// 命令描述
          .action((name, options, cmd) => {
            console.log('執(zhí)行 create 命令');
          });

        program.on('--help', () => {
          console.log();
          console.log(`Run ${chalk.cyan('rippi <command> --help')} to show detail of this command`);
          console.log();
        });

        program
          // 說明版本
          .version(`rippi-cli@${config.version}`)
          // 說明使用方式
          .usage('<command [option]');

        // 解析用戶執(zhí)行命令傳入的參數(shù)
        program.parse(process.argv);

        將上面提到的第三方庫都安裝一下,然后隨便打開一個(gè)cmd,執(zhí)行 cli create project。

        步驟三:完善核心命令---create命令

        上面的步驟都只是一個(gè)腳手架最基本的鋪墊,而create命令才是最關(guān)鍵的,而這最核心的create命令都應(yīng)該做些什么事情呢?

        這里就要聊聊腳手架的本質(zhì)了,腳手架的本質(zhì)無非就是我們先在一個(gè)倉庫里寫好一個(gè)模板項(xiàng)目,然后腳手架每次運(yùn)行的時(shí)候都把這個(gè)模板項(xiàng)目拉到目標(biāo)項(xiàng)目中,腳手架不過是省去了我們拉代碼,初始化項(xiàng)目的操作而已。那么現(xiàn)在,create命令的基本流程就是這樣了。

        image.png

        ps: 如果要使用gitee的話,就不能使用download-git-repo這個(gè)庫了,這個(gè)庫只支持下載github,要另外找一個(gè)支持下載gitee的庫

        • 創(chuàng)建一個(gè)lib文件夾,任何工具方法或者抽象類都放到這個(gè)文件夾中。以下是代碼,注釋解釋的都比較清楚了。
        // lib/creator.js 編寫一個(gè)creator類,整個(gè)找模板到下載模板的主要邏輯都抽象到了這個(gè)類中。
        import { fetchRepoList } from './request.js';
        import { loading } from './utils.js';
        import downloadGitRepo from 'download-git-repo';
        import inquirer from 'inquirer';
        import chalk from 'chalk';
        import util from 'util';

        class Creator {
          constructor(projectName, targetDir) {
            this.name = projectName;
            this.dir = targetDir;
            // 將downloadGitRepo轉(zhuǎn)成promise
            this.downloadGitRepo = util.promisify(downloadGitRepo);
          }

          fetchRepo = async () => {
            const branches = await loading(fetchRepoList, 'waiting for fetch resources');
            return branches;
          }

          fetchTag = () => {}

          download = async (branch) => {
            // 1 拼接下載路徑 這里放自己的模板倉庫url
            const requestUrl = `rippi-cli-template/react/#${branch}`;
            // 2 把資源下載到某個(gè)路徑上
            await this.downloadGitRepo(requestUrl, this.dir);
            console.log(chalk.green('done!'));
          }

          create = async () => {
            // 1 先去拉取當(dāng)前倉庫下的所有分支
            const branches = await this.fetchRepo();
            // 這里會(huì)在shell命令行彈出選擇項(xiàng),選項(xiàng)為choices中的內(nèi)容
            const { curBranch } = await inquirer.prompt([
              {
                name'curBranch',
                type'list',
                // 提示信息
                message'please choose current version:',
                // 選項(xiàng)
                choices: branches
                  .filter((branch) => branch.name !== 'main')
                  .map((branch) => ({
                    name: branch.name,
                    value: branch.name,
                  })),
              },
            ]);
            // 2 下載
            await this.download(curBranch);
          }
        };

        export default Creator;

        // lib/utils.js 給異步方法加loading效果,只是一個(gè)好看點(diǎn)的交互效果
        import ora from 'ora';

        export const loading = async (fn, msg, ...args) => {
          // 計(jì)數(shù)器,失敗自動(dòng)重試最大次數(shù)為3,超過3次就直接返回失敗
          let counter = 0;
          const run = async () => {
            const spinner = ora(msg);
            spinner.start();
            try {
              const result = await fn(...args);
              spinner.succeed();
              return result;
            } catch (error) {
              spinner.fail('something go wrong, refetching...');
              if (++counter < 3) {
                return run();
              } else {
                return Promise.reject();
              }
            }
          };
          return run();
        };

        // lib/request.js 下載倉庫

        import axios from 'axios';

        axios.interceptors.response.use((res) => {
          return res.data;
        });

        // 這里是獲取模板倉庫的所有分支,url寫自己的模板倉庫url
        export const fetchRepoList = () => {
          return axios.get('https://api.github.com/repos/rippi-cli-template/react/branches');
        };

        寫完上述代碼,接下來我們實(shí)例化下creator,然后調(diào)用它的create方法就好了。

        // lib/create.js
        import path from 'path';
        import Creator from './creator.js';

        /**
         * 執(zhí)行create時(shí)的處理
         * @param {any} name // 創(chuàng)建的項(xiàng)目名
         * @param {any} options // 配置項(xiàng) 必須是上面option配置的選項(xiàng)之一,否則就報(bào)錯(cuò)  這里取的起始就是cmd里面的options的各個(gè)option的long屬性
         * @param {any} cmd // 執(zhí)行的命令本身 一個(gè)大對(duì)象,里面很多屬性
         */

        const create = async (projectName, options, cmd) => {
          // 獲取工作目錄
          const cwd = process.cwd();
          // 目標(biāo)目錄也就是要?jiǎng)?chuàng)建的目錄
          const targetDir = path.join(cwd, projectName);
          // 創(chuàng)建項(xiàng)目
          const creator = new Creator(projectName, targetDir);
          creator.create();
        };

        export default create;

        // bin/index.js 將上文中的action改掉
        program
          .command('create <app-name>')  // 創(chuàng)建命令
          .description('create a new project'// 命令描述
          .action((name, options, cmd) => {
            console.log('執(zhí)行 create 命令');
          });

        那么好,完成上述動(dòng)作,我們來看看效果。

        在一個(gè)空文件夾中打開shell命令行,然后執(zhí)行cli create project project是項(xiàng)目名,隨便改。

        image.png

        效果已經(jīng)出來了,我的這個(gè)倉庫有兩個(gè)分支,分別是react和react+ts的模板分支,這里任意選一個(gè)。

        image.png

        選擇完畢之后,就會(huì)開始下載,看到done就說明下載完了。

        image.png

        此時(shí)我們的文件夾中多了這么一個(gè)文件夾,打開進(jìn)去看。

        image.png

        就是我們模板倉庫里面的那些文件內(nèi)容。

        其實(shí)到這里,最基本的一個(gè)腳手架就寫完了,不過對(duì)于嘗試了多次的朋友來說會(huì)發(fā)現(xiàn)一個(gè)問題,那就是當(dāng)當(dāng)前文件夾中存在相同名稱的文件時(shí),文件就直接被覆蓋,而很多時(shí)候這個(gè)行為是不好的,會(huì)導(dǎo)致用戶丟失不想丟失的內(nèi)容,為了優(yōu)化這個(gè)體驗(yàn)我們加個(gè)--force的配置。

        優(yōu)化:增加--force配置

        force,就當(dāng)遇到同名文件,直接覆蓋繼續(xù)我們的創(chuàng)建項(xiàng)目的流程。

        // bin/index.js  新增一個(gè)option
        program
          .command('create <app-name>')  // 創(chuàng)建命令
          .description('create a new project'// 命令描述
          .option('-f, --force''overwrite target directory if it is existed'// 命令選項(xiàng)(選項(xiàng)名,描述) 這里就是解決下重名的情況
          .action((name, options, cmd) => {
            import('../lib/create.js').then((default: create }) => {
              create(name, options, cmd);
            });
          });

        在create方法中,我們接受的第二參數(shù)就會(huì)包含這個(gè)option。

        // lib/create.js
        import path from 'path';
        import fs from 'fs-extra';
        import inquirer from 'inquirer';
        import Creator from './creator.js';

        /**
         * 執(zhí)行create時(shí)的處理
         * @param {any} name // 創(chuàng)建的項(xiàng)目名
         * @param {any} options // 配置項(xiàng) 必須是上面option配置的選項(xiàng)之一,否則就報(bào)錯(cuò)  這里取的起始就是cmd里面的options的各個(gè)option的long屬性
         * @param {any} cmd // 執(zhí)行的命令本身 一個(gè)大對(duì)象,里面很多屬性
         */

        const create = async (projectName, options, cmd) => {
          // 先判斷是否重名,如果重名,若選擇了force則直接覆蓋之前的目錄,否則報(bào)錯(cuò)
          // 獲取工作目錄
          const cwd = process.cwd();
          // 目標(biāo)目錄也就是要?jiǎng)?chuàng)建的目錄
          const targetDir = path.join(cwd, projectName);
          if (fs.existsSync(targetDir)) {
            // 選擇了強(qiáng)制創(chuàng)建,先刪除舊的目錄,然后創(chuàng)建新的目錄
            if (options.force) {
              await fs.remove(targetDir);
            } else {
              const { action } = await inquirer.prompt([
                {
                  name'action',
                  type'list',
                  // 提示信息
                  message`${projectName} is existed, are you want to overwrite this directory`,
                  // 選項(xiàng)
                  choices: [
                    { name'overwrite'valuetrue },
                    { name'cancel'valuefalse },
                  ],
                },
              ]);
              if (!action) {
                return;
              } else {
                console.log('\r\noverwriting...');
                await fs.remove(targetDir);
                console.log('overwrite done');
              }
            }
          }

          // 創(chuàng)建項(xiàng)目
          const creator = new Creator(projectName, targetDir);
          creator.create();
        };

        export default create;

        整個(gè)create方法增加多了一個(gè)判斷是否存在同名文件的情況。

        ps:node其實(shí)已經(jīng)不推薦使用exists相關(guān)的方法了,但為了好理解這里仍然使用這個(gè)方法。node更推薦的是access方法,想了解更多可以查閱node官方文檔。

        增加完這段邏輯之后,我們這個(gè)腳手架的完整流程如下:

        image.png

        結(jié)尾

        本文是腳手架搭建的一個(gè)入門,這個(gè)腳手架只擁有最簡單的功能,而下一篇腳手架的搭建將會(huì)是復(fù)雜版的,擁有者插件機(jī)制,能通過配置插件動(dòng)態(tài)生成項(xiàng)目,比如是初始化各種lint、是否使用mobx/redux,亦或者是是否初始化路由等,這都能通過配置插件完成,敬請期待吧??。

        那么好,本文到此就結(jié)束了,希望沒接觸過腳手架的朋友能通過這篇文章了解到腳手架并且實(shí)現(xiàn)自己的腳手架。

        本文簡單腳手架的完整代碼:點(diǎn)這里[8]

        原文鏈接:https://juejin.cn/post/7260893255189758010


        Node 社群

            


        我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

           “分享、點(diǎn)贊、在看” 支持一下

        瀏覽 80
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            人人操人人干人人操 | 午夜在线一区二区在线视频网站免费 | 精品视频| 激情视频网址 | 三级片网站在线播放 | 少妇每次做都很紧 | 青草社区视频在线观看 | 国产精品久久久久久模特 | 青青操青青碰熟女视频 | 色爱综合 |