解锁创新潜力:Farm 核心 API 带你打造个性化构建工具

写在前面

本文开始将开始 Farm 框架原理性解析的第一篇, 带领大家从 0 到 1 到最后打造出属于自己的编译器。

如果还有不知道 Farm 的同学,在这里再给大家简单的介绍一下 Farm 是一个基于 Rust 实现的极速构建引擎,帮助您更快地构建 Web 程序 和 JavaScript 库。

Farm 仓库相关链接, 希望大家可以多多支持, 如果大家对 Farm 感兴趣, 也非常希望大家可以参与进来, 一同打造下一代 Web 构建工具

Farm 相关链接, 希望大家多多鼓励和支持

Farm CLI

第一篇文章带大家创建基于 Farm api 来打造一套个性化的构建工具, 带大家先从 cli 命令行开始, 不仅仅可以像其他构建工具一样比如 vite , 使用 vite vite build vite preview 命令来快速启动一个项目,我们也可以针对不同功能,业务来封装属于自己的命令

那么什么是 cli 呢 ?, 以及针对 web 项目的 cli 和传统意义上的 cli 有什么区别呢

CLI是Command Line Interface的缩写,即命令行界面。它是一种在计算机上与用户进行交互的方式,用户通过键入文本命令来执行各种操作,而不是通过图形用户界面(GUI)。
而针对 Web 构建工具的CLI通常是一组命令行工具,用于辅助开发人员在项目中执行各种任务。这些任务可以包括启动服务器运行项目、构建项目、预览项目, 分析等。

开发 Cli 使用的工具

开发cli也有一些好用的工具,这里为大家介绍一些作者最常用的工具。

  • commander: 用于解析命令和参数,可以给不同的命令配置不同的处理函数,函数会接收到这个伴随命令传入的参数,可以说是非常好用了。
  • citty : 优雅的 CLI 构建器由 unjs 开发, 函数式编程,各个环节逻辑清晰
  • Oclif:是一个用于构建CLI的框架,它提供了一组强大的工具和约定,可以帮助你快速构建出具有一致性和专业性的CLI工具。它支持命令的组合、自动生成帮助文档、命令插件化等功能。
  • cac: 是一个用于构建 CLI 应用程序的 JavaScript 库。也是目前比较常用的一款
  • ora: 控制台的loading效果
  • minimist 解析命令行参数
  • inquirer: 可以在控制台和用户一问一答,交互式的处理。

实现功能

本文会从头搭建项目, 带领大家一步一步完成整个 cli 项目的构建与发布,以下是我们要实现的几个通用命令, 因为本文只是 cli 的建设搭建所以不会对具体核心 api 以及功能实现展开讲解, 以后 Farm 团队都会一点一点以本文为起点解析所有功能, 请大家拭目以待。

以下是Farm目前所有实现的一些主要命令, 本文带大家主要完成 startbuild 命令

  • farm start 启动开发服务器
  • farm build 项目打包编译
  • farm watch 监听项目打包编译,修改代码后实时进行编译
  • farm preview 启动预览服务器来预览生产环境产物
  • farm clean 清除增量构建写入磁盘的缓存

项目初始化

接下来就正式开始创建我们的项目, 那么我们选择 citty 来开发脚手架,至于为什么选择 citty 因为不仅 citty 基于 mri (轻量 parser 解析器) mri 比其他以前常用的 这是 minimistyargs-parser 的都要快很多, 并且使用了更加现代化的函数式编程思想,提供了defineCommandrunMain 等函数 API,语法更加简洁直观。而很多其他工具使用的是基于原型或对象的API方式.

  • 初始化项目
ts 复制代码
pnpm init
  • 安装项目所需依赖, 尽量做到轻量, 功能全面, 代码清晰简洁
ts 复制代码
pnpm add @farmfe/core citty -D
  • package.json 中设置 bin 字段, 在package.jsonbin 字段指定一个可执行文件的路径,npm 会为该文件创建一个软链接,添加到 系统环境变量PATH 中。这样在终端就可以直接执行该命令。
ts 复制代码
{
  "name": "personal-next-cli",
  "version": "1.0.0",
  "main": "./dist/index.cjs",
  "types": "./dist/index.d.ts",
  "bin": {
    "next": "./bin/cli.js"
  }
}
  • 在项目中设置 bin 目录, 新增一个 cli.js 文件使用shebang语法 , 在cli.js入口文件的第一行加上了#!/usr/bin/env node。这是一种Unix风格的shebang语法,告诉系统使用env命令调用node解释器来执行该脚本文件。这使得cli.js可以像其他命令行程序一样被直接执行。
js 复制代码
#!/usr/bin/env node
console.log("Im next personal cli");

然后终端中执行 pnpm install -g 或者 npm link 将包安装到全局环境后, bin 中配置的命令就被链接到了 系统的PATH变量 中。这样在任何目录下执行该命令都是可行的,方便使用。我们执行 next 之后, 就会运行当前 bin 链接的 js 文件

ts 复制代码
PS D:\adny\personal-cli> next // 命令行输入
Im next personal cli // 输出

所以我们只需要把 console.log("Im next personal cli"); 这句话换成我们需要运行的文件就可以完成我们的第一步了

  • 那么先来介绍一下 citty 提供的主要方法及其作用:
  1. defineCommand

defineCommand是citty中定义命令的核心API。它接收一个对象作为参数,该对象描述了命令的元数据、参数配置和执行逻辑。返回一个命令定义对象。

ts 复制代码
const main = defineCommand({
    meta: { /*...*/ }, // 命令元数据解析 例如 version
    args: { /*...*/ }, // 命令参数配置 
    run({ args }) { /*...*/ } // 命令执行逻辑 
})
  1. runMain

runMain用于运行一个命令定义对象。它会根据用户输入的参数执行相应的命令逻辑,并自动生成帮助信息。这是运行CLI应用的入口点。

ts 复制代码
const main = defineCommand({
    meta: { /*...*/ }, // 命令元数据解析 例如 version
    args: { /*...*/ }, // 命令参数配置 
    run({ args }) { /*...*/ } // 命令执行逻辑 
})

runMain(main) // 运行命令
  1. showUsage

呈现使用方法并打印到控制台

ts 复制代码
PS D:\adny\personal-cli> next --help

USAGE next-personal-cli [OPTIONS] start|build|watch|preview|clean

OPTIONS

  -h, --help    Show this help message

COMMANDS

    start    Compile the project in dev mode and serve it with dev server
    build    Compile the project in production mode
    watch    Watch file change and recompile
  preview    Compile the project in watch mode with preview server
    clean    Clean up the cache built incrementally

Use next-personal-cli <command> --help for more information about a command.
  1. parseArgs

parseArgs用于解析命令行参数,并应用默认值。返回解析后的参数对象。

ts 复制代码
const args = parseArgs(['--foo', 'bar'], { ... }) // { _: [], foo: 'bar' }

常用的命令我们只需要前两个 (defineCommand , runMain)就够了

然后 @farmfe/core 作为 farm 的核心包, 提供了很多的编译方面的 api 在后续文章我们都会为大家介绍, @farmfe/core 也内置了针对 cli 所需要的所有 api 包括 start, build, watch, clean, preview 和其他更加底层的 api 例如 compiler, wathcer, server 等等

编写代码

目录结构如下

ts 复制代码
-- src 
  -- index.ts 
  -- command 
    -- build.ts 
    -- preview.ts 
    -- start.ts 
    -- watch.ts 
    -- clean.ts
-- package.json
-- bin
  -- cli.js

先来看入口文件 src/index.ts, 先定义所有命令 citty 使用 defineCommand 定义入口, meta 定义元数据, 比如我们可以传入脚手架的版本号或者描述,run 方法代表每次执行命令之后执行的函数, subCommands 定义子命令。

ts 复制代码
import fs from 'node:fs'
import {  defineCommand, runMain, showUsage } from 'citty'
import { args } from './args.js'

const packageJsonFile = fs.readFileSync('./package.json', 'utf8');
const { version, description } = packageJsonFile as any;

const main = defineCommand({
  meta: {
    name: 'next-personal-cli',
    version,
    description,
  },
  args,
  async run(ctx) {
    if (ctx.args.help) {
      await showUsage(ctx.cmd)
      return
    }
  },
  subCommands: {
    start: () => import("./commands/start.js").then((r) => r.default),
    build: () => import("./commands/build.js").then((r) => r.default),
    watch: () => import("./commands/watch.js").then((r) => r.default),
    preview: () => import("./commands/preview.js").then((r) => r.default),
    clean: () => import("./commands/clean.js").then((r) => r.default),
  },
});

runMain(main);

arg 属性代表每一个命令传递的参数, 可以传递类型,别名,默认值,还有描述,比如第一个 config 参数, 那么比如在我们运行 build 的时候 就可以使用 next build --config next-cli.config.ts 来指定我们需要运行的配置文件, farm 默认接收一个 farm.config.ts 文件会把定义的配置文件和命令行参数进行合并, 然后解析出最终交给 compiler 的参数对象

ts 复制代码
export const args: any = {
  config: {
    type: 'string',
    alias: 'c',
    description:
      'Use this config file (if argument is used but value is unspecified, defaults to farm.config.ts)',
  },
  mode: {
    type: 'string',
    alias: 'm',
    description: 'Set env mode',
    default: 'development',
  },
  base: {
    type: 'string',
    description: 'Public base path',
  },
  clearScreen: {
    type: 'boolean',
    description: 'Allow/disable clear screen when logging',
    default: true,
  },
  help: {
    type: 'boolean',
    alias: 'h',
    description: 'Show this help message',
  },
}

start

那么先来看一下 start 子命令, farm 提供了开箱即用的一些命令, 也包含了所有编译,开发服务器等 api 的使用方式, 在 start 方法中就可以使用 farm 提供的 start 方法, 传递不同的参数

ts 复制代码
import { start } from '@farmfe/core'
import { startArgs as args } from "../args.js";

export default defineCommand({
  meta: {
    name: "start",
    description: "Compile the project in dev mode and serve it with dev server",
  },
  args,
  run({args}) {
    const resolveOptions = resolveCommandOptions(args);
    const configPath = getConfigPath(args.config as string);

    const defaultOptions: any = {
      compilation: {
        lazyCompilation: args.lazy
      },
      server: resolveOptions,
      clearScreen: args.clearScreen,
      configPath,
      mode: args.mode
    };
    
    start(defaultOptions as any)
  },
});

先来设计一下 start 子命令所需要的参数

    1. base : 配置 dev serverpublic path
    1. port: 支持命令行传递不同端口号
    1. host : 支持命令行传递 host 主机
    1. open:支持默认自动打开浏览器功能

start 的命令行参数一般情况下, 我们会可能需要配置 base 相当于开发服务器 public path, port 开发服务器端口号,host 开发服务器主机 ip 地址, open 是否默认启动浏览器。我们先简单的使用这几个

ts 复制代码
export const startArgs: any = {
  base: {
    type: 'string',
    description: 'Public base path',
  }
  host: {
    type: "number",
    description: "specify host",
  },
  port: {
    type: "number",
    description: "specify port",
  },
  open: {
    type: "boolean",
    description: "open browser after server started",
  },
}

然后我们就可以开始测试 start 命令了, 我们简单的使用 tsc 进行打包, 然后 bin 文件链接打包之后的 dist 目录。

先定义 tsconfig.json 文件

json 复制代码
{
  "compilerOptions": {
    "noImplicitAny": true,
    "sourceMap": true,
    "target": "esnext",
    "inlineSources": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist",
    "declaration": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "resolveJsonModule": true,
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "node",
    "module": "esnext"
  },
  "exclude": [
    "node_modules",
    "src/templates",
  ],
  "include": [
    "src/**/*.ts"
  ],
}

然后运行 tsc 之后会生成 dist 目录, 我们在 bin/cli.mjs 中导入这个文件

shell 复制代码
#!/usr/bin/env node
import '../dist/index.js'

package.json 中已经配置了 bin 属性的指向,所以当我们运行 next start 他就会执行这个文件, 接下来新建一个 playground 目录,新建一个 index.html

然后我们进入到 playground 目录运行 next start --port 6542

成功运行,说明我们 start 方法就已经运行成功了, 那么我们 start 命令就完成了, 大家根据公司业务或者其他想法, 也可以添加其他的命令行参数。

Build

接下来开始 build 命令, 其实功能也是一样, 浅浅设计一下 build 命令

    1. outDir: 支持编译打包之后的输出目录
    1. entry: 支持编译的入口文件
    1. format : 如果是 node 环境需要打包出不同的输出格式比如 esm, cjs
    1. targetEnv : 要同时支持 web 环境 和 node 环境的打包
    1. minify : 是否需要压缩 minify
    1. sourcemap : 是否输出生成 sourcemap
    1. treeShaking : 以及是否需要 treeShaking

那么需要传递的命令行参数就是

ts 复制代码
export const buildArgs: any = {
  // 输出目录
  outDir: {
    type: 'string',
    alias: 'o',
    description: 'Output directory',
  },
  // 入口文件
  input: {
    type: 'string',
    alias: 'i',
    description: 'Input file',
  },
  // 构建环境
  target: {
    type: 'string',
    alias: 't',
    description: 'transpile targetEnv node, browser',
  },
  // 格式化输出 esm / cjs
  format: {
    type: 'string',
    alias: 'f',
    description: 'Output format esm, cjs',
  },
  // 是否开启压缩
  minify: {
    type: 'boolean',
    description: 'code compression at build time',
  },
  // 是否开始 sourcemap
  sourcemap: {
    type: 'boolean',
    description: 'output source maps for build'
  },
  // 是否开启树摇
  treeShaking: {
    type: 'boolean',
    description: 'Eliminate useless code without side effects',
  },
}

然后开始编写 buildsubCommands, 把格式化后的参数传递给 farm 提供的 build 方法

javascript 复制代码
import { defineCommand } from "citty";
import { build } from '@farmfe/core'

export default defineCommand({
  meta: {
    name: "build",
    description: "Compile the project in production mode",
  },
  args,
  run({ args }) {
    const configPath = getConfigPath(args.config);
    const defaultOptions = {
      compilation: {
        watch: args.watch,
        output: {
          path: args?.outDir,
          targetEnv: args?.target,
          format: args?.format
        },
        input: {
          index: args?.input
        },
        sourcemap: args.sourcemap,
        minify: args.minify,
        treeShaking: args.treeShaking
      },
      mode: args.mode,
      configPath
    };
    build(defaultOptions)
  },
});

这样我们在执行 next build 之后他就会自动执行这个命令

接下来在测试一下我们定义的 build 命令行参数

shell 复制代码
next build --entry index.js --outDir build --target node-next --sourcemap

然后目录就会生成 build 文件夹,并且打包的是 node 环境下的产物

Farm 一致性

Farm 提供的这些开箱即用的方法,我们拿startbuild举例,其他命令原理也是如此, 他们都是传递一个配置参数对象, 这两个方法除了 start 是需要启动一个开发服务器, 其他基本没有区别, 除了一些默认的生产环境以及开发环境的插件 比如默认开发环境不开启压缩以及树摇,所以你甚至可以在开发环境中开启 treeShakingminifyFarm 的这一举动是为了表达出开发环境和生产环境永远的一致性

扩展 create 命令

其他命令原理也是一样的,这样就完成了一个构建工具基本的 cli 启动和构建的能力。 为了扩展 cli 的能力,在带领大家写一个创建模板的子命令, 首先在 index.ts 中添加create 子命令

ts 复制代码
  subCommands: {
    create: () => import("./commands/create.js").then((r) => r.default),
  },

commands 目录下新建 create.ts 文件整个功能的实现也非常简单, 我们提供好已知的项目模板, 只要根据不同的命令行参数就可以解析到不同的 template 最后把模板复制到我们传递的目录中就可以了, 我们使用 prompts 这个包来进行交互不仅提供命令行参数的能力, 也提供模板交互来让我们手动进行选择, 我们为了简化操作就只提供一种选择框架的交互 整个代码逻辑也非常简单,

我们需要先安装 prompts 交互和 picocolors 颜色包

ts 复制代码
pnpm add prompts picocolors -D

代码逻辑如下,createArgs 我们可以传递 template 参数来指定使用 vue 或者 react 等其他框架或者根据 prompts 来进行手动选择,先输入项目名称, 然后选择需要使用的框架, 然后就成功啦。

ts 复制代码
import { defineCommand } from "citty";
import fs from 'node:fs'
import prompts from 'prompts';
import colors from 'picocolors';
import { createArgs as args } from "../args.js";
import { copyTemplate, formatTargetDir, getConfigPath, isEmpty, pkgFromUserAgent, resolveCommandOptions } from "../utils.js";

export default defineCommand({
  meta: {
    name: "create template",
    description: "create a new project from a template with farm",
  },
  args,
  run({ args }) {
    createFarm(args)
  },
});

async function createFarm(args: any) {
  const cwd = process.cwd();
  const DEFAULT_TARGET_NAME = 'farm-project';
  const argProjectName = formatTargetDir(args._[0]);
  const argFramework = args.template || args.t;
  let targetDir = argProjectName || DEFAULT_TARGET_NAME;
  let result: IResultType = {};
  const skipInstall = args['skip-install'] ?? args.skipInstall ?? true;
  try {
    result = await prompts(
      [
        {
          type: argProjectName ? null : 'text',
          name: 'projectName',
          message: 'Project name:',
          initial: DEFAULT_TARGET_NAME,
          onState: (state) => {
            targetDir = formatTargetDir(state.value) || DEFAULT_TARGET_NAME;
          }
        },
        {
          type: () =>
            !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
          name: 'overwrite',
          message: () =>
            (targetDir === '.'
              ? '🚨 Current directory'
              : `🚨 Target directory "${targetDir}"`) +
            ` is not empty. Overwrite existing files and continue?`
        },
        {
          type: (_, { overwrite }: { overwrite?: boolean }) => {
            if (overwrite === false) {
              throw new Error(colors.red('❌') + ' Operation cancelled');
            }
            return null;
          },
          name: 'overwriteChecker'
        },
        {
          type: argFramework ? null : 'select',
          name: 'framework',
          message: 'Select a framework:',
          initial: 0,
          choices: [
            {
              title: colors.cyan('React'),
              value: 'react'
            },
            { title: colors.green('Vue'), value: 'vue' },
          ]
        },
      ],
      {
        onCancel: () => {
          throw new Error(colors.red('❌') + ' Operation cancelled');
        }
      }
    );
  } catch (cancelled) {
    console.log(cancelled.message);
    return;
  }
  const { framework = argFramework, packageManager } = result;

  await copyTemplate(targetDir, {
    framework,
    projectName: targetDir,
    packageManager
  });

  console.log(colors.green('📦 Template copied Successfully!'));
}

接下来我们试一下效果, 要提前把 template 文件夹放到目录中, 发布的时候可以在 package.json 中放到 files

我们定义好的 templates 目录

然后在任意目录下输入

ts 复制代码
next create

选择完之后就能看到项目被正确的创建了

总结

本文从头到尾详细介绍了如何使用 Farm 构建一个个性化的 CLI 工具,旨在帮助开发者快速构建适用于特定项目或场景的命令行界面工具。通过本文的学习,读者不仅可以了解到 CLI 工具的重要性和特点,还能够掌握使用 citty 构建 CLI 工具的方法。

在项目初始化和结构部分,我们学习了如何使用 citty 初始化项目,配置 package.jsonbin 字段,并创建了项目的目录结构,为后续各个子命令的实现做好了准备。接着,我们逐步实现了 startbuild 等子命令,展示了如何利用 Farm 提供的 API 来完成具体功能。整个脚手架的命令都已经完成本文只带大家体验了 startbuild 命令。其他的 watchpreviewclean 完整的功能可以看github next-personal-cli 仓库代码

在最后,我们还介绍了如何添加交互式命令 create,并使用 prompts 包来与用户进行交互,以便根据用户的选择创建新项目。这一部分为 CLI 工具增添了更多的交互性和可定制性,使其更加灵活和易用。

我们甚至可以基于 cli 扩展各种各样的插件命令来快速创建插件,Farm 框架也提供了更多核心、功能颗粒度更加细致的 API,使得开发者可以根据自身需求定制更加完善的功能。不仅仅是 cli, 无论是扩展现有功能、引入新的特性,还是开发更专业化的工具,都可以在Farm的基础上灵活实现。这种灵活性和可扩展性为开发者提供了丰富的选择,使得他们可以根据项目需求和个人偏好,打造出更符合实际场景的定制化工具。因此,无论是构建简单的 CLI 工具还是复杂的 Web 应用,Farm 都能够提供强大支持,并为开发者带来更大的创造空间。

本文实现代码功能逻辑并不复杂, 适合大家在步入前端工程化开头的一课,这是我们实现 Farm 的第一课,后面的代码会循序渐进。请大家拭目以待,毫无保留的教给大家

欢迎加入 Farm 团队

欢迎加入大家加入 Farm 团队来一起共同建设。欢迎大家 Star🌟

Farm 官网:Farm

Farm 代码仓库:Farm Code

Farm 交流群, 欢迎大家加入我们的交流群共同学习,共同进步

相关推荐
golitter.6 分钟前
Ajax和axios简单用法
前端·ajax·okhttp
雷特IT26 分钟前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
长路 ㅤ   1 小时前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
亚里士多没有德7751 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010141 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw1 小时前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
九圣残炎1 小时前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱2 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^2 小时前
C语言习题~day16
c语言·前端·算法