从 0 实现一个前端项目提效脚手架

简介: 从 0 实现一个前端项目提效脚手架

1 功能演示

1.1 脚手架安装

npm i @coderyjw/cli -g

输入 jw-cli -h 查询帮助

image.png

1.2 init 命令演示:下载项目模板

1. 输入 jw-cli init -h 查询帮助

image.png

2. 输入 jw-cli init初始化项目,选择类型:项目

image.png

3. 输入项目名称: react-app

image.png

4. 选择项目模板:react模板

image.png

5. 下载成功

image.png

1.3 install 命令演示:源码下载

1. 输入 jw-cli install,选择 Gitee

image.png

2. 输入 token

image.png

3. 输入关键词: vue-admin-template

image.png

4. 输入开发语言(可选择不输入)

image.png

5. 选择项目:vue-admin-template

image.png

6. 选择版本: 4.3.1

image.png

image.png

image.png

7. 下载成功,启动项目

image.png

1. 脚手架整体框架搭建

1.1 脚手架入口文件开发

1. 使用 Lerna 创建项目

Lerna 是一个用于管理包含多个包的 JavaScript 项目的管理工具。

npm i lerna -g
mkdir jw-cli
cd jw-cli
lerna init

此时我们的目录结构应该是这样的:

image.png

2. 通过 Lerna 创建 cli package

lerna create cli

3. 创建 packages/cli/bin/cli.js,通常来说脚手架文件都会放在 bin 目录下

4. 将 packages/cli/lib/cli.js 修改为 packages/cli/lib/index.js。这个就是我们未来要下载和安装的入口文件

5. 到 npm 上创建对应的组织

image.png

image.png
6. 修改 packages/cli/package.json

image.png

注意上面填入的 package name(@coderyjw/cli) 要填入你自己在 npm 上创建的组织,

7. 让入口文件生效

cd packages/cli
npm link

8. 修改 bin/cli.jslib/index.js 测试是否生效

bin/cli.js

#!/usr/bin/env node

import entry from "../lib/index.js";

entry(process.argv.slice(2));

lib/index.js

export default function (args) {
  console.log("this is entry", args);
}

执行 jw-cli,控制台打印如下内容说明入口文件已生效

image.png

至此我们脚手架的入口就已经开发好了。

1.2 脚手架注册 + 命令注册

1. 在 packages/cli 目录下安装 commander 插件

lerna add commander packages/cli

commander 是一个很好用的 node.js 命令行解决方案,这里就不详细讲了。

2. 创建 packages/cli/lib/createCLI.js 文件用于封装脚手架注册逻辑

import fse from "fs-extra";
import path from "path";
import { dirname } from "dirname-filename-esm";

const __dirname = dirname(import.meta);
const pkgPath = path.resolve(__dirname, "../package.json");
const pkg = fse.readJsonSync(pkgPath);

export default function createCLI(program) {

  // 命令注册
  program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version, "-v, --version", "输出当前版本号")
    .option("-d, --debug", "是否开启调试模式", false)
    .helpOption("-h, --help", "命令显示帮助")
    .addHelpCommand("help [command]", "命令显示帮助");

  // 监听未知的命令
  program.on("command:*", function (obj) {
    console.error("未知的命令:" + obj[0]);
  });

  // 监听 debug 选项
  program.on("option:debug", function () {
    if (program.opts().debug) {
      console.log("开启调试模式");
    }
  });
}

3. 在 packages/cli/lib/index.js 中引入 createCLI

import { program } from "commander";
import createCLI from "./createCLI.js";

export default function (args) {
  createCLI(program);
  program.parse(process.argv);
}

这里需要引入两个包:

  1. fs-extra:它可以看做是 nodefs 模块的升级版,比 fs 更加的好用
  2. dirname-filename-esm:因为在 esm 中,是没有 cjs 中的 __dirname 变量的,但是我们可以通以下下方法获得 __dirname
import { dirname } from "dirname-filename-esm";
const __dirname = dirname(import.meta);

所以我们要先安装以下这两个包

lerna add dirname-filename-esm  packages/cli
lerna add fs-extra  packages/cli

4. 最后测试能出现下面的效果则说明我们已经成功注册了命令

image.png

1.3 log 日志功能封装

在上面的代码中,我们使用的是 cosole.log 进行日志打印,实际上我们有更好的选择,那就是 npmlog,虽然它的功能已经非常完善了,但是我们最好对它再进行一层封装。

1. 创建 utils 文件夹作为我们的一个工具包,后续我们所有的工具类都放在这个包下

lerna create utils

此时的目录结构

image.png

2. 打开 packages/utils/packages.json 修改

image.png

3. 将 packages/utils/utils.js 改为 packages/utils/index.js

4. 安装 npmlog 依赖

leran add npmlog pacakges/utils

5. 创建 packages/utils/log.js 文件并做如下配置

import log from "npmlog";
import isDebug from "./isDebug.js";

if (isDebug()) {
  log.level = "verbose";
} else {
  log.level = "info";
}

log.heading = "jw-cli";

log.addLevel("success", 2000, { fg: "green", bold: true });

export default log;

6. 创建 packages/utils/isDebug.js

/**
 * 判断是否是 debug 模式
 * @returns boolean
 */
export default function isDebug() {
  return process.argv.includes("--debug") || process.argv.includes("-d");
}

7. 在 packages/utils/index.js 导入并导出这两个模块

import isDebug from "./isDebug.js";
import log from "./log.js";

export { log, isDebug };

8. 在 cli 包中引入 utils

lerna add @coderyjw/utils packages/cli

9. 修改 packages/cli/bin/createCLI.js,引入 log 模块,并将 console 改为 log

import { log } from "@coderyjw/utils";
...
export default function createCLI(program) {
  ...
  // 监听未知的命令
  program.on("command:*", function (obj) {
    log.error("未知的命令:" + obj[0]);
  });

  // 监听 debug 选项
  program.on("option:debug", function () {
    if (program.opts().debug) {
      log.verbose("debug", "开启调试模式");
    }
  });

10. 测试成功

image.png

1.4 Commander 类封装

log 一样,虽然 commander 已经很好用了,但是我们还是希望能对其进行封装,这样对我们后续的命令注册也会非常的方便。对于 Commander 类的封装,这里提供给大家一种思路。

1. 创建 command 一个新的 package

lerna create command

2. 修改 packages/command/package.json

image.png

3. 将 packages/command/command.js 改为 packages/command/index.js,修改 index.js

class Command {
  constructor(instance) {
    if (!instance) {
      throw new Error("command instance must not be null!");
    }

    this.program = instance;

    const cmd = this.program.command(this.command);
    cmd.description(this.description);
    cmd.hook("preAction", () => {
      this.preAction();
    });
    cmd.hook("postAction", () => {
      this.postAction();
    });
    if (this.options?.length > 0) {
      this.options.forEach((option) => {
        cmd.option(...option);
      });
    }
    cmd.action((...params) => {
      this.action(params);
    });
  }

  get command() {
    throw new Error("command must be implements");
  }

  get description() {
    throw new Error("description must be implements");
  }

  get options() {
    return [];
  }

  action() {
    throw new Error("action must be implements");
  }

  preAction() {
    // empty
  }

  postAction() {
    // empty
  }
}

export default Command;

在上面的代码中,我们封装了一个 Command 类最后将其导出,在 Command 类的构造函数中,我们主要做了以下几件事:

  1. 通过 program.command 注册命令
  2. 添加 description 命令描述信息
  3. 添加了命令生命周期 preActionpostAction 两个 hook,这两个函数分别会在 action 的前后触发
  4. 调用 command.action() 触发我们命令的逻辑

之后的所有命令只要继承该类即可

1.5 init 命令封装

接下来我们终于可以开始我们的第一个命令的注册了。

1. 创建 init

lerna create init

2. 修改 packages/init/package.json

image.png

3. 将 packages/init/init.js 改为 packages/init/index.js,修改 index.js

import Command from "@coderyjw/command";
import { log } from '@coderyjw/utils'

class InitCommand extends Command {
  get command() {
    return "init [name]";
  }

  get description() {
    return "项目初始化";
  }

  get options() {
    return [
      ["-f, --force", "是否强制更新", false],
      ["-t, --type <type>", "项目类型(project/page)"],
      ["-tp, --template <template>", "模版名称"],
    ];
  }

  async action([name, opts]) {
    log.verbose("init", name, opts);
  }

  preAction() {}

  postAction() {}
}

function Init(instance) {
  return new InitCommand(instance);
}

export default Init;

在上面的代码中我们封装的 InitCommand 继承自 Command 类,最后导出了一个创建 InitCommand 类的函数,这样,我们只要到 packages.cli/lib/index.js 脚手架入口文件中调用这个函数即可

4. 修改 packages.cli/lib/index.js

import { program } from "commander";
import createCLI from "./createCLI.js";
import createInitCommand from "@coderyjw/init";

export default function (args) {
  createCLI(program);

  createInitCommand(program)
  
  // 解析配置
  program.parse(process.argv);
}

5. 当然我们还要导入一下所需的依赖

lerna add @coderyjw/command packages/init
lerna add @coderyjw/utils packages/init
lerna add @coderyjw/init packages/cli

6. 测试成功

image.png

1.6 node 最低版本检查功能

到现在我们脚手架的基本逻辑其实已经差不多了,我们可以再回来优化一下启动逻辑,比如检查 Node 版本,这个启动的逻辑可以放在 commander 的钩子函数 preAction 中来实现

1. 修改 packages/cli/bin/createCLI.js

import semver from "semver";
import chalk from "chalk";
...
...
const LOWEST_NODE_VERSION = "14.0.0";

const checkNodeVersion = () => {
  log.verbose("node version", process.version);
  // 当前 node 版本低于 LOWEST_NODE_VERSION 时 抛出错误
  if (!semver.gte(process.version, LOWEST_NODE_VERSION)) {
    throw new Error(
      chalk.red(`jw-cli 需要安装 ${LOWEST_NODE_VERSION} 以上版本的 Node.js`)
    );
  }
};

const preAction = () => {
  // 检查 Node 版本
  checkNodeVersion();
};
export default function createCLI(program) {
  // 命令注册
  program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version, "-v, --version", "输出当前版本号")
    .option("-d, --debug", "是否开启调试模式", false)
    .helpOption("-h, --help", "命令显示帮助")
    .addHelpCommand("help [command]", "命令显示帮助")
    .hook("preAction", preAction);
...
...
  • 在上面的代码中:

    1. 我们首先定义了一个 preAction 函数,后续如果还有什么启动逻辑需要做的都可以放在这里,
    2. 然后封装了一个 checkNodeVersion,在这个方法中我们用到了两个第三方包

      1. 一个是 semver: 这个包是专门用来比较两个版本的
      2. 另一个是 chalk:这个包是专门用来修改终端字符串样式的

2. 接下来我们安装下这两个包

lerna add chalk packages/cli 
lerna add semver packages/cli 

3. 分别修改 LOWEST_NODE_VERSION 的值为 17.0.014.0.0 进行测试,测试成功

image.png

image.png

1.7 添加异常监听

到现在为止,我们已经基本把脚手架的框架维护好了,最后再做一些收尾工作,比如异常监听。

Node.js 程序运行在单进程上,应用开发时一个难免遇到的问题就是异常处理,

关于异常监听,我们可以这样优化,判断如果当前是调试模式时再打印出函数栈,如果不是就仅打印出 message 即可

1. 修改 packages/utils/lib/log.js,封装 printErrorLog 方法,之后如果我们主动捕捉到的异常时都可以用这个方法打印日志

export function printErrorLog(e, type) {
  if (isDebug()) {
    log.error(type, e);
  } else {
    log.error(type, e.message);
  }
}

2. 修改在 packages/utils/lib/index.js , 导出 printErrorLog

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";

export { log, isDebug, printErrorLog };

3. 另外除了主动捕捉到的异常,还有一些可能漏掉的异常,可能导致程序退出,我们创建 packages/cli/lib/exception.js

import { printErrorLog } from "@coderyjw/utils";

process.on("uncaughtException", (e) => printErrorLog(e, "error"));

process.on("unhandledRejection", (e) => printErrorLog(e, "promise"));

4. 在 packages/cli/index.js 中引入 exception.js

import "./exception.js";

5. 我们以上一小节的报错为例进行测试

image.png

可以看到加上 -d 的有报错的函数调用栈,而没加的就只打印出了报错 message

2. 项目创建脚手架开发

  • 我们为什么要开发创建项目的脚手架?为什么不直接使用 vue-cli 或者 create-react-app

    1. 因为项目在迭代过程中会添加很多本土化的元素,如:H5 兼容接口请求埋点上报组件封装通用方法等,甚至会对整块业务逻辑进行复用,比如登录;
    2. 而且每次在创建项目的时候都要重新填写这些代码是非常耗时的,而且无法对每个团队成员进行复用,可以利用脚手架来完成项目模板的沉淀和标准化建设。

2.1 项目模板开发

1. 新建一个 jw-cli-template 项目,并按如下目录结构创建文件夹

image.png

2. 修改分别修改各个模板的 package.json

react 模板

{
  "name": "@coderyjw/template-react18",
  "version": "1.0.0",
  "description": "jw-cli react18 template",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

vue 模板

{
  "name": "@coderyjw/template-vue3",
  "version": "1.0.0",
  "description": "jw-cli vue3 template",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

vue-element-admin 模板

{
  "name": "@coderyjw/template-vue-element-admin",
  "version": "1.0.0",
  "description": "jw-cli vue-element-admin template",
  "main": "index.js",
  "author": "yejw6",
  "license": "ISC",
  "publishConfig": {
    "access": "public"
  }
}

3. 这三个模板的 template 就是存放我们最终代码的位置,关于模板大家可以按照自己的沉淀配置,这里我分享一下我 react模板的配置

4. 通过 npm 依次发布模板

npm publish 

5. 可以在 npm 仓库中看到我们的三个模板

image.png

2.2 整体逻辑搭建

有了模板之后我们就可以开始写代码去下载模板了。

  • 我们的这个脚手架最主要的逻辑应该包含以下三条:

    1. 我们应该有一个命令行来让我们选择生成哪份模板,甚至还可以选择生成一个页面模块(包含页面、状态、路由、网络请求等等)
    2. 选择完之后最好是能缓存到本地硬盘上,这样下次再去生成时就不要再去下载了
    3. 最后从缓存中下载到本地目录,提示命令运行项目

1. 创建 packages/init/lib/createTemplate.js

import { log } from "@coderyjw/utils";

export default async function createTemplate(name, opts) {
  log.verbose("createTemplate", "选择项目模板,生成项目信息");

  return null
}

2. 创建 packages/init/lib/downloadTemplate.js

import { log } from "@coderyjw/utils";

export default async function downloadTemplate(selectedTemplate) {
  log.verbose("downloadTemplate", "下载项目模板值缓存目录");
  log.verbose("template", selectedTemplate);
}

3. 创建 packages/init/lib/installTemplate.js

import { log } from "@coderyjw/utils";

export default async function installTemplate(name, opts) {
  log.verbose("installTemplate", "安装项目模板至目录");
}

上面创建的三份文件分别对应我们上面说的三个逻辑,目前还是空的需要我们去完善代码。

4. 修改 packages/init/lib/index.js

import createTemplate from "./createTemplate.js";
import downloadTemplate from "./downloadTemplate.js";
import installTemplate from "./installTemplate.js";
...
...

  async action([name, opts]) {
    // 1. 选择项目模板,生成项目信息
    const selectedTemplate = await createTemplate(name, opts);
    // 2. 下载项目模板值缓存目录
    await downloadTemplate(selectedTemplate);
    // 3. 安装项目模板至目录
    await installTemplate(selectedTemplate, opts);
  }

至此我们整体的代码已经创建好了,接下来就是去实现各个逻辑了

2.3 选择项目模板,生成项目信息

要想在命令行进行选择模板,就要用到 inquirer 了,inquirer 拥有一组通用的交互式命令行用户界面,支持常见的 input 输入、单选、多选、是/否等常见提问类型

1. 创建 packages/utils/inquirer.js,分别对其以上能力进行封装

import inquirer from "inquirer";

function make({
  choices,
  defaultValue,
  message = "请选择",
  type = "list",
  require = true,
  mask = "*",
  validate,
  pageSize,
  loop,
}) {
  const options = {
    name: "name",
    default: defaultValue,
    message,
    type,
    require,
    mask,
    validate,
    pageSize,
    loop,
  };

  if (type === "list") {
    options.choices = choices;
  }

  return inquirer.prompt(options).then((answer) => answer.name);
}

export function makeList(params) {
  return make({ ...params });
}

export function makeInput(params) {
  return make({ type: "input", ...params });
}

export function makePassword(params) {
  return make({ type: "password", ...params });
}

2. 在 package/utils/lib/index.js 中导出

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";
import { makeList, makeInput, makePassword } from "./inquirer.js";

export { log, isDebug, printErrorLog, makeList, makeInput, makePassword };

3. 安装 inquirer

lerna add inquirer packages/utils

4. 修改 package/init/lib/createTemplate.js 文件

import { log, makeList, makeInput } from "@coderyjw/utils";
const ADD_TYPE_PROJECT = "project";
const ADD_TYPE_PAGE = "page";

const ADD_TYPE = [
  { name: "项目", value: ADD_TYPE_PROJECT },
  { name: "页面", value: ADD_TYPE_PAGE },
];

const GLOBAL_ADD_TEMPLATE = [
  {
    name: "vue3 项目模板",
    value: "template-vue3",
    npmName: "@coderyjw/template-vue3",
    version: "1.0.1",
  },
  {
    name: "react18 项目模板",
    value: "template-react18",
    npmName: "@coderyjw/template-react18",
    version: "1.0.0",
  },
  {
    name: "vue-element-admin 项目模板",
    value: "template-vue-element-admin",
    npmName: "@coderyjw/template-vue-element-admin",
    version: "1.0.0",
  },
];

// 获取创建类型
function getAddType() {
  return makeList({
    choices: ADD_TYPE,
    message: "请选择初始化类型",
    defaultValue: ADD_TYPE_PROJECT,
  });
}

// 获取项目名称
function getAddName() {
  return makeInput({
    message: "请输入项目的名称",
    defaultValue: "",
    validate(name) {
      if (name.length > 0) return true;
      return "项目名称不能为空";
    },
  });
}

// 选择项目模版
function getAddTemplate(ADD_TEMPLATE) {
  return makeList({
    choices: ADD_TEMPLATE,
    message: "请选择项目模版",
  });
}

export default async function createTemplate(name, opts) {
  log.verbose("createTemplate", "选择项目模板,生成项目信息");

  const ADD_TEMPLATE = GLOBAL_ADD_TEMPLATE;

  // 项目类型,项目名称,项目模版
  let addType, addName, addTemplate;
  addType = await getAddType();
  log.verbose("addType", addType);

  if (addType === ADD_TYPE_PROJECT) {
    addName = await getAddName();
    log.verbose("addName", addName);
    addTemplate = await getAddTemplate(ADD_TEMPLATE);
    log.verbose("addTemplate", addTemplate);

    const selectedTemplate = ADD_TEMPLATE.find((_) => _.value === addTemplate);
    if (!selectedTemplate) throw new Error(`项目模版 ${template} 不存在!`);
    log.verbose("selectedTemplate", selectedTemplate);

    return {
      type: addType,
      name: addName,
      template: selectedTemplate,
    };
  } else {
    throw new Error(`抱歉,创建的项目类型 ${addType} 暂不支持!`);
  }
}

在上面的代码中我们分别封装 getAddName getAddType getAddTemplate 三个方法,用于获取要创建的类型、名称和模板

5. 测试

image.png

image.png

image.png

image.png

不出意外的话就会生成我们选中模板的信息了。

6. 修改 package/init/lib/createTemplate.js,优化代码,使之能通过一条命令直接生成模板

+  const { type, template } = opts;
   // 项目类型,项目名称,项目模版
   let addType, addName, addTemplate;
-  addType = await getAddType()
+  if (type) {
+    addType = type;
+  } else {
+    addType = await getAddType();
+  }
...
-    addName = await getAddName();
+    if (name) {
+      addName = name;
+    } else {
+      addName = await getAddName();
+    }
-    addTemplate = await getAddTemplate(ADD_TEMPLATE);
+    if (template) {
+      addTemplate = template;
+    } else {
+      addTemplate = await getAddTemplate(ADD_TEMPLATE);
+    }

7. 测试成功

image.png

上面的代码还会有一个小问题,就是生成的模板信息中 version 一直是 1.0.0 固定,我们知道我们的版本是会更新的,如果每次更新都要重新改一下脚手架的代码是会十分麻烦的,那么应该如何解决呢?我这里的提供一种方法是每次都去下载最新的版本,或者你也可以做一个功能下载指定版本的模板。

npm 给我们提供了一个 api 让我们可以获取到一个包的所有信息,我们只需要通过请求 https://registry.npmjs.org/ + 包名 即可

image.png

8. npm API 接入,创建 packages/utils/npm.js

import urlJoin from "url-join";
import axios from "axios";
import { log } from "./log.js";

function getNpmInfo(npmName) {
  // npm API 地址 https://registry.npmjs.org/
  // 如果 npm 过慢,可以使用淘宝镜像 https://registry.npmmirror.com/
  const registry = "https://registry.npmmirror.com/";
  const url = urlJoin(registry, npmName);
  return axios.get(url).then((res) => {
    try {
      return res.data;
    } catch (e) {
      Promise.reject(e);
    }
  });
}

export default async function getLatestVersion(npmNmae) {
  const result = await getNpmInfo(npmNmae);
  if (result?.["dist-tags"]?.["latest"]) {
    return result["dist-tags"]["latest"];
  }
  log.error("没有 latest 版本号");
  return Promise.reject("没有 latest 版本号");
}

9. 修改 packages/utils/index.js 导出 getLatestVersion 方法

import isDebug from "./isDebug.js";
import log, { printErrorLog } from "./log.js";
import { makeList, makeInput, makePassword } from "./inquirer.js";
import getLatestVersion from "./npm.js";
export {
  log,
  isDebug,
  printErrorLog,
  makeList,
  makeInput,
  makePassword,
  getLatestVersion,
};

10. 最后不要忘了安装 axiosurl-join 两个包

lerna add axios packages/utils
lerna add url-join packages/utils

11. 在 packages/init/lib/createTemplate.js 中引入 getLatestVersion,获取最新版本

// 获取最新的版本
const latestVersion = await getLatestVersion(selectedTemplate.npmName);
log.verbose("latestVersion", latestVersion);
selectedTemplate.version = latestVersion;

12. 最后因为我们之后要下载模板,最好将缓存目录获取的逻辑在这一步给实现了传过去

import path from "path";
import { homedir } from "os";

const TEMP_HOME = ".jw-cli";
...
// 安装缓存目录
function makeTargetPath() {
  return path.resolve(`${homedir()}/${TEMP_HOME}`, "addTemplate");
}
...
const targetPath = makeTargetPath();
log.verbose("targetPath", targetPath);

return {
  type: addType,
  name: addName,
  template: selectedTemplate,
  targetPath,
};

2.4 下载项目模板至缓存目录

接下来我们就来到了第二步逻辑:下载项目模板至缓存目录

1. 修改 packages/init/downloadTemplate.js

import path from "node:path";
import { pathExistsSync } from "path-exists";
import fse from "fs-extra";
import ora from "ora";
import { execa } from "execa";
import { printErrorLog, log } from "@yejiwei/utils";

function getCacheDir(targetPath) {
  return path.resolve(targetPath, "node_modules");
}

function makeCacheDir(targetPath) {
  const cacheDir = getCacheDir(targetPath);
  if (!pathExistsSync(cacheDir)) {
    fse.mkdirpSync(cacheDir);
  }
}

async function downloadAddTemplate(targetPath, selectedTemplate) {
  const { npmName, version } = selectedTemplate;
  const installCommand = "npm";
  const installArgs = ["install", `${npmName}@${version}`];
  const cwd = targetPath;
  const subprocess = execa(installCommand, installArgs, { cwd });
  await subprocess;
}

export default async function downloadTemplate(selectedTemplate) {
  const { targetPath, template } = selectedTemplate;
  makeCacheDir(targetPath);
  const spinner = ora("正在下载模版....").start();

  try {
    await downloadAddTemplate(targetPath, template);
    spinner.stop();
    log.success("模版下载成功");
  } catch (e) {
    spinner.stop();
    printErrorLog(e);
  }
}
  • 在上面的代码中:

    1. 我们主要通过封装了 downloadAddTemplate 来下载模板
    2. 引入 execa模块 来执行 npm install 包名的方式将模板下载至缓存目录中
    3. 另外还引入了 ora 模块做了一个下载中的动画

    image.png

2. 引入这些依赖

lerna add path-exists packages/init
lerna add execa packages/init
lerna add ora packages/init

3. 输入命令测试

image.png

image.png

可以看到命令行显示模板下载成功,并且模板代码已经成功下载至我们的路径 C:\Users\yjw\.jw-cli\addTemplate\node_modules\@coderyjw\template-react18 下面

2.5 安装项目模板至目录

接下来我们终于来到第三步逻辑了:安装项目模板至目录

1. 修改 packages/init/lib/installTemplate.js

import fse from "fs-extra";
import path from "path";
import ora from "ora";
import { log } from "@coderyjw/utils";

function getCacheFilePath(targetPath, template) {
  return path.resolve(targetPath, "node_modules", template.npmName, "template");
}

function copyFile(targetPath, template, installDir) {
  const originFilePath = getCacheFilePath(targetPath, template);
  log.verbose("originFilePath", originFilePath);
  const fileList = fse.readdirSync(originFilePath);
  log.verbose("fileList", fileList);
  const spinner = ora("正在拷贝文件...").start();
  fileList.map((file) => {
    fse.copySync(`${originFilePath}/${file}`, `${installDir}/${file}`);
  });
  spinner.stop();
  log.success("模版拷贝成功");
}

export default async function installTemplate(selectedTemplate, opts) {
  log.verbose("installTemplate", "安装项目模板至目录");
  const { force = false } = opts;
  const { targetPath, template, name } = selectedTemplate;
  const rootDir = process.cwd();
  fse.ensureDirSync(targetPath);
  const installDir = path.resolve(`${rootDir}/${name}`);
  fse.ensureDirSync(installDir);

  copyFile(targetPath, template, installDir);
  log.info(`输入以下命令运行项目`)
  log.info(`cd ${name}`)
  log.info(`npm install`)
  log.info(`npm start`)
}

在上面的代码中我们主要封装了 copyFile 方法用来从缓存目录中拷贝文件到当前目录下,但是有一个小问题就是,如果当前目录下我们填入 name 的文件夹已存在时就会有点问题,这里我们可以通过添加一个 option 让用户选择是否强制更新

image.png

2. 代码逻辑优化

+   import { pathExistsSync } from "path-exists";
...
+   const { force = false } = opts;
...
-  fse.ensureDirSync(installDir);
+  if (pathExistsSync(installDir)) {
+    if (!force) {
+      log.error(`当前目录已存在 ${installDir} 文件夹`);
+      return;
+    } else {
+      fse.removeSync(installDir);
+      fse.ensureDirSync(installDir);
+    }
+  } else {
+    fse.ensureDirSync(installDir);
+  }

3. 输入 jw-cli init react-app -t project -tp template-react18 -d 命令测试

image.png
模板成功安装

再次输入命令显示

image.png

输入 jw-cli init react-app -t project -tp template-react18 -d -f 命令强制更新

image.png

至此,我们的项目创建脚手架的开发就已经完成了。

3. 源码下载脚手架开发

在上一章中,我们完成了项目创建脚手架的开发,代码全部放置 init文件夹下,从本章开始我们将进行源码下载器的开发。

  • 为什么要实现源码下载器?

    1. 因为 npm 只能下载 npm registry 中的包,而对于未上传 npm 的包则无法下载
    2. 对于 githubgitee 的项目或包如果手动下载效率低下,则可以通过脚手架开发前端项目下载器提升效率
  • 那么,实现一个源码下载器需要怎么样的流程呢?
  1. 首先我们需要掌握 github APIgitee API的使用方法
  2. 通过 githubgitee 实现根据仓库名称搜索项目的能力(github 能力更强,甚至可以根据源码进行搜索)
  3. 存储 githubgiteetoken ,具备 github APIgitee API的调用条件
  4. 通过搜索获取仓库信息,选取指定版本(实现翻页功能)
  5. 将指定版本的源码拉取到本地
  6. 在本地安装依赖,安装可执行文件,启动项目

3.1 整理逻辑搭建

1. 创建 install 文件夹作为源码下载器的包

lerna create install

2. 修改 packages/install/package.json

{
  "name": "@coderyjw/install",
  "version": "0.0.0",
  "description": "jw-cli脚手架 install 命令",
  "author": "叶继伟 <yejw6@asiainfo.com>",
  "homepage": "",
  "license": "ISC",
  "main": "lib/index.js",
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": [
    "lib"
  ],
  "type": "module",
  "scripts": {
    "test": "node ./__tests__/install.test.js"
  }
}

3. 修改 packages/install/lib/install.jspackages/install/lib/index.js

import Command from "@coderyjw/command";


class InstallCommand extends Command {
  get command() {
    return "install";
  }

  get description() {
    return "项目下载、安装依赖、启动项目";
  }

  get options() {
    return [["-c, --clear", "清空缓存", false]];
  }

  async action() {}
}

function Install(instance) {
  return new InstallCommand(instance);
}

export default Install;

4. 在 packages/cli/lib/index.js 中引入 install

import { program } from "commander";
import createCLI from "./createCLI.js";
import createInitCommand from "@coderyjw/init";
import createInstall from "@coderyjw/install";
import "./exception.js";

export default function (args) {
  createCLI(program);

  createInitCommand(program);

  createInstall(program)

  // 解析配置
  program.parse(process.argv);
}

5. 安装依赖

lerna add @coderyjw/command packages/install
lerna add @coderyjw/install packages/cli

6. 运行 jw-cli,可以看到此时我们的脚手架已经有了 install 命令

image.png

3.2 平台选择 + token 缓存

1. 创建 packages/utils/git/GitServer.js 文件

import { homedir } from "os";
import path from "path";
import { pathExistsSync } from "path-exists";
import fse from "fs-extra";
import { makePassword } from "../lib/inquirer.js";
import { log } from "../lib/index.js";

const TEMP_HOME = ".jw-cli";
const TEMP_GITHUB_TOEN = ".github-token";
const TEMP_GITEE_TOEN = ".gitee-token";

function createTokenPath(platForm) {
  if (platForm === "github") {
    return path.resolve(homedir(), TEMP_HOME, TEMP_GITHUB_TOEN);
  } else {
    return path.resolve(homedir(), TEMP_HOME, TEMP_GITEE_TOEN);
  }
}

export default class GitServer {
  constructor() {}

  async init(platForm) {
    // 判断 token 是否录入
    const tokenPath = createTokenPath(platForm);
    if (pathExistsSync(tokenPath)) {
      this.token = fse.readFileSync(tokenPath).toString();
    } else {
      this.token = await this.getToken();
      fse.writeFileSync(tokenPath, this.token);
    }
    log.verbose("token", this.token);
  }

  async getToken() {
    return await makePassword({ message: "请输入 token 信息" });
  }
}

2. 创建 packages/utils/git/Gitee.js 文件

import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://gitee.com/api/v5";

export default class Gitee extends GitServer {
  constructor() {
    super();
  }
}

3. 创建 packages/utils/git/Github.js 文件

import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://api.github.com";

export default class GitHub extends GitServer {
  constructor() {
    super();
  }
}

4. 创建 packages/utils/git/GitUtils.js 文件

import { makeList, log } from "../lib/index.js";
import Github from "./Github.js";
import Gitee from "./Gitee.js";

export async function chooseGitPlatForm() {
  let platForm = await makeList({
    message: "请选择 Git 平台",
    choices: [
      {
        name: "Github",
        value: "github",
      },
      { name: "Gitee", value: "gitee" },
    ],
  });
  log.verbose("platForm", platForm);

  return platForm;
}

export async function initGitServer(platForm) {
  let gitAPI;
  if (platForm === "github") {
    gitAPI = new Github();
  } else if (platForm === "gitee") {
    gitAPI = new Gitee();
  }
  await gitAPI.init(platForm);
  return gitAPI;
}

5. 在 packages/utils/index.js 中导入并导出

+  import Github from "../git/Github.js";
+  import Gitee from "../git/Gitee.js";
+  import { chooseGitPlatForm, initGitServer } from "../git/GitUtils.js";
export {
  log,
  isDebug,
  printErrorLog,
  makeList,
  makeInput,
  makePassword,
  getLatestVersion,
+  Github,
+  Gitee,
+  chooseGitPlatForm,
+  initGitServer
};

在上面的代码中我们分别封装了三个类 GitSeverGithubGitee,后两者继承自前者,我们之后会使用这两个类来接入 giteegithubapi 来完成源码的查询与下载,但是,我们当前的目标还是先进行平台(github/gitee)的选择和 token 的缓存,因为调用这两个平台的 api 都是需要提供它们的 token 的,所以我们最好在第一次输入之后缓存一下,之后就不用再输入了。

6. 修改 packages/install/lib/index.js

import { chooseGitPlatForm, initGitServer } from "@coderyjw/utils";
...

  async action() {
    await this.generateGitAPI();
  }

  async generateGitAPI() {
    this.platForm = await chooseGitPlatForm();

    this.gitAPI = await initGitServer(this.platForm);
  }
...

7. 安装依赖

lerna add path-exists packages/utils
lerna add fs-extra packages/utils
lerna add @coderyjw/utils packages/install

8. 测试

image.png

image.png

我们到 C:\Users\yjw\.jw-cli 目录下可以看到两个平台的 token 数据都已经缓存到了本地

image.png

  1. 关于 Github token 如何获取 token 可以查看 文档
  2. Gitee 创建 token

3.3 仓库搜索 + 翻页功能开发

1. Github / Gitee Search API 接入,分别修改 packages/utils/git/Github.jspackages/utils/git/Gitee.js

Github.js

import axios from "axois";
import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://api.github.com";

export default class GitHub extends GitServer {
  constructor() {
    super();
    this.service = axios.create({
      baseURL: BASE_URL,
      timeout: 50000,
    });

    this.service.interceptors.request.use(
      (config) => {
        config.headers["Authorization"] = `Bearer ${this.token}`;
        config.headers["Accept"] = "application/vnd.github+json";
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    this.service.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (err) => {
        return Promise.reject(err);
      }
    );
  }

  get(url, params, headers) {
    return this.service({
      url,
      params,
      method: "GET",
      headers,
    });
  }

  post(url, data, headers) {
    return this.service({
      url,
      data,
      params: {
        access_token: this.token,
      },
      method: "post",
      headers,
    });
  }

  searchRepositories(params) {
    return this.get("search/repositories", params);
  }

  searchCode(params) {
    return this.get("search/code", params);
  }
}

Gitee.js

import axios from "axois";
import GitServer from "./GitServer.js";
import { log } from "../lib/index.js";

const BASE_URL = "https://gitee.com/api/v5";

export default class Gitee extends GitServer {
  constructor() {
    super();
    this.service = axios.create({
      baseURL: BASE_URL,
      timeout: 5000,
    });

    this.service.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (err) => {
        return Promise.reject(err);
      }
    );
  }

  get(url, params, headers) {
    return this.service({
      url,
      params: {
        ...params,
        access_token: this.token,
      },
      method: "GET",
      headers,
    });
  }

  post(url, data, headers) {
    return this.service({
      url,
      data,
      params: {
        access_token: this.token,
      },
      method: "post",
      headers,
    });
  }

  searchRepositories(params) {
    return this.get("search/repositories", params);
  }
}

2. 安装 axios

lerna add axios packages/utils

3. 修改 packages/install/lib/index.js

import Command from "@coderyjw/command";
import {
  chooseGitPlatForm,
  initGitServer,
  makeList,
  makeInput,
} from "@coderyjw/utils";
const PREV_PAGE = "prev_page";
const NEXT_PAGE = "next_page";
const SEARCH_MODE_REPO = "search_mode_repo";
const SEARCH_MODE_CODE = "search_mode_code";
...
  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();
    log.verbose("selectedProject", this.selectedProject);
  }
  ...
  async searchGitAPI() {
    log.verbose("this.platForm", this.platForm);
    if (this.platForm === "github") {
      this.mode = await makeList({
        message: "请选择搜索模式",
        choices: [
          { name: "仓库名称", value: SEARCH_MODE_REPO },
          { name: "源码", value: SEARCH_MODE_CODE },
        ],
      });
    }

    // 1. 收集搜索关键词和开发语言
    this.q = await makeInput({
      message: "请输入搜索关键词",
      validate(value) {
        if (value) {
          return true;
        } else {
          return "请输入搜索关键词";
        }
      },
    });

    this.language = await makeInput({
      message: "请输入开发语言",
    });

    this.keywords =
      this.q + (this.language ? `+language:${this.language}` : "");

    log.verbose("search keywords", this.keywords, this.platForm);

    this.page = 1;
    this.perPage = 10;

    await this.doSearch();
  }

  async doSearch() {
    // 2. 根据平台生成搜索参数
    let params;
    let count = 0;
    let list;
    let searchResult;

    if (this.platForm === "github") {
      params = {
        q: this.keywords,
        order: "desc",
        per_page: this.perPage,
        page: this.page,
      };

      log.verbose("search project params", params);
      log.verbose("mode", this.mode);
      if (this.mode === SEARCH_MODE_REPO) {
        searchResult = await this.gitAPI.searchRepositories(params);
        list = searchResult.items.map((item) => ({
          name: `${item.full_name}(${item.description})`,
          value: item.full_name,
        }));
      } else if (this.mode === SEARCH_MODE_CODE) {
        searchResult = await this.gitAPI.searchCode(params);
        list = searchResult.map((item) => ({
          name:
            item.repository.full_name +
            (item.repository.description &&
              `(${item.repository.description})`),
          value: item.repository.full_name,
        }));
      }

      count = searchResult.total_count; // 整体数据量
    } else if (this.platForm === "gitee") {
      params = {
        q: this.q,
        order: "desc",
        per_page: this.perPage,
        page: this.page,
      };
      if (this.language) {
        params.language = this.language; // 注意输入格式: JavaScript
      }
      log.verbose("search params", params);
      searchResult = await this.gitAPI.searchRepositories(params);

      count = 99999;

      list = searchResult.map((item) => ({
        name: `${item.full_name}(${item.description})`,
        value: item.full_name,
      }));
    }
    // 判断当前页面,已经是否达到最大页数
    if (
      (this.platForm === "github" && this.page * this.perPage < count) ||
      (this.platForm === "gitee" && list?.length > 0)
    ) {
      list.push({
        name: "下一页",
        value: NEXT_PAGE,
      });
    }

    if (this.page > 1) {
      list.unshift({
        name: "上一页",
        value: PREV_PAGE,
      });
    }

    if (count > 0) {
      const selectedProject = await makeList({
        message:
          this.platForm === "github"
            ? `请选择要下载的项目(共 ${count} 条数据)`
            : "请选择要下载的项目",
        choices: list,
      });

      if (selectedProject === NEXT_PAGE) {
        await this.nextPage();
      } else if (selectedProject === PREV_PAGE) {
        await this.prevPage();
      } else {
        // 选中项目 去查询 tag
        this.selectedProject = selectedProject;
      }
    }
  }

  async nextPage() {
    this.page++;
    await this.doSearch();
  }

  async prevPage() {
    this.page--;
    await this.doSearch();
  }
  • 解释上面的代码:

    1. 我们首先调用了封装的 searchGitAPI 方法,这个方法是专门查询源码仓库的
    2. searchGitAPI 我们先是进行了平台的判断,因为 github 提供的 api 是可以通过源码查询仓库的
    3. 之后我们进行了一些数据的收集,包括搜索的关键字和开发语言,这些都是调用 github/gitee api 所需要的参数
    4. 然后调用了另一个封装的 doSearch 方法,封装这个方法的目的是为了翻页,因为在翻页的时候还需要再次做查询的操作,代码会重复。
    5. doSearch 方法中我们对不同平台(github/gitee)不同搜索模式(通过仓库名称搜索、通过源码搜索)进行了区分,它们会调用不同的 api
    6. 再拿到搜索到的数据之后,在命令行中以列表的形式显示出来让用户选择
    7. 最后分别封装了 nextPageprePage 进行翻页的操作

4. 我们通过 jw-cli install -d 命令进行测试

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3.4 tag 选择 + 翻页功能开发

tag 的选择和翻页的功能和上一小节是相同的逻辑,这里就不再解释,直接看代码吧

1. 修改 packages/utils/Gitee.js,接入查询 giee tag 的 api

getTags(fullName) {
    return this.get(`/repos/${fullName}/tags`);
}

2. 修改 packages/utils/Github.js,接入查询 github tag 的 api

getTags(fullName, params) {
    return this.get(`/repos/${fullName}/tags`, params);
}

3. 修改 packages/install/lib/index.js

...
  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();
    log.verbose("selectedProject", this.selectedProject);
    log.verbose("selectedTag", this.selectedTag);
  }
  ...
  async searchGitAPI() {
      ...
      ...
      await this.selectTags();
  }
...
  async selectTags() {
    let tagsList;
    this.tagPage = 1;
    this.tagPerPage = 30;
    tagsList = await this.doSelectTags();
  }

  async doSelectTags() {
    let tagsListChoices = [];
    let tagsList
    if (this.platForm === "github") {
      const params = {
        page: this.tagPage,
        per_page: this.tagPerPage,
      };
      log.verbose("search tags params", this.selectedProject, params);
       tagsList = await this.gitAPI.getTags(this.selectedProject, params);
      tagsListChoices = tagsList.map((item) => ({
        name: item.name,
        value: item.name,
      }));
    } else {
      log.verbose("search tags params", this.selectedProject);
       tagsList = await this.gitAPI.getTags(this.selectedProject);
      tagsListChoices = tagsList.map((item) => ({
        name: item.name,
        value: item.name,
      }));
    }
    if (tagsList.length === 0) return;

    if (tagsList.length > 0) {
      tagsListChoices.push({
        name: "下一页",
        value: NEXT_PAGE,
      });
    }
    if (this.tagPage > 1) {
      tagsListChoices.unshift({
        name: "上一页",
        value: PREV_PAGE,
      });
    }
    const selectedTag = await makeList({
      message: "请选择tag",
      choices: tagsListChoices,
    });

    if (selectedTag === NEXT_PAGE) {
      await this.nextTags();
    } else if (selectedTag === PREV_PAGE) {
      await this.prevTags();
    } else {
      this.selectedTag = selectedTag;
    }
  }

  async prevTags() {
    this.tagPage--;
    await this.doSelectTags();
  }

  async nextTags() {
    this.tagPage++;
    await this.doSelectTags();
  }

4. 测试

image.png

3.5 下载指定分支源码

选择完仓库名和分支后就可以去下载仓库了,这个功能我们可以通过 execa 模块执行 git clone 命令实现

  1. 修改 packages/install/lib/index.js

  async action() {
    await this.generateGitAPI();

    await this.searchGitAPI();

    log.verbose("selectedProject", this.selectedProject);
    log.verbose("selectedTag", this.selectedTag);

+    await this.downloadRepo();
  }
  
  ...
+  async downloadRepo() {
+    const spinner = ora(
+      `正在下载:${this.selectedProject}` +
+        (this.selectedTag ? `(${this.selectedTag})` : "")
+    ).start();
+    try {
+      await this.gitAPI.cloneRepo(this.selectedProject, this.selectedTag);
+      spinner.stop();
+      log.success(
+        `下载模板成功:${this.selectedProject}` +
+          (this.selectedTag ? `(${this.selectedTag})` : "")
+      );
+    } catch (err) {
+      spinner.stop();
+      printErrorLog(err);
+    }
+  }
  1. 修改 packages/utlis/git/GitServer.js
+  cloneRepo(fullName, tag) {
+    if (tag) {
+      return execa("git", ["clone", this.getRepoUrl(fullName), "-b", tag]);
+    }
+    return execa("git", ["clone", this.getRepoUrl(fullName)]);
+  }
  1. 修改 packages/utlis/git/Github.js
+  getRepoUrl(fullName) {
+    return `https://github.com/${fullName}.git`;
+  }
  1. 修改 packages/utlis/git/Gitee.js
+  getRepoUrl(fullName) {
+    return `https://gitee.com/${fullName}.git`;
+  }
  1. 安装依赖
lerna add ora packages/install

3.6 自动安装依赖 + 启动项目

  1. 修改 packages/install/lib/index.js
  async downloadRepo() {
    const spinner = ora(
      `正在下载:${this.selectedProject}` +
        (this.selectedTag ? `(${this.selectedTag})` : "")
    ).start();
    try {
      await this.gitAPI.cloneRepo(this.selectedProject, this.selectedTag);
      spinner.stop();
      log.success(
        `下载模板成功:${this.selectedProject}` +
          (this.selectedTag ? `(${this.selectedTag})` : "")
      );
+      await this.installDependencies();
+      await this.runRepo();
    } catch (err) {
      spinner.stop();
      printErrorLog(err);
    }
  }
  
+  async installDependencies() {
+    const spinner = ora(
+      `正在安装依赖:${this.selectedProject}` +
+        (this.selectedTag ? `(${this.selectedTag})` : "")
+    ).start();
+    try {
+      const ret = await this.gitAPI.installDependencies(
+        process.cwd(),
+        this.selectedProject,
+        this.selectedTag
+      );
+      spinner.stop();
+      if (ret) {
+        log.success(
+          `依赖安装安装成功:${this.selectedProject}` +
+            (this.selectedTag ? `(${this.selectedTag})` : "")
+        );
+      } else {
+        log.error("依赖安装失败");
+      }
+    } catch (err) {
+      spinner.stop();
+      printErrorLog(err);
+    }
+  }

  async runRepo() {
    await this.gitAPI.runRepo(process.cwd(), this.selectedProject);
  }
  1. 修改 packages/utils/git/GitServer.js

function getProjectPath(cwd, fullName) {
  const projectName = fullName.split("/")[1]; // vuejs/vue => vue
  const projectPath = path.resolve(cwd, projectName);
  return projectPath;
}

function getPackageJson(cwd, fullName) {
  const projectPath = getProjectPath(cwd, fullName);
  const pkgPath = path.resolve(projectPath, "package.json");
  if (pathExistsSync(pkgPath)) {
    return fse.readJsonSync(pkgPath);
  } else {
    return null;
  }
}

...
export default class GitServer {
...
  installDependencies(cwd, fullName, tag) {
    const projectPath = getProjectPath(cwd, fullName);
    if (pathExistsSync(projectPath)) {
      return execa("npm", ["install", "--registry=https://registry.npmmirror.com"], { cwd: projectPath });
    }

    return null;
  }

  runRepo(cwd, fullName) {
    const projectPath = getProjectPath(cwd, fullName);
    const pkg = getPackageJson(cwd, fullName);
    if (pkg) {
      const { scripts, bin } = pkg;

      if (bin) {
        execa("npm", ["run", "-g", name, "--registry=https://registry.npmmirror.com"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      }

      if (scripts && scripts.dev) {
        return execa("npm", ["run", "dev"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else if (scripts && scripts.serve) {
        return execa("npm", ["run", "serve"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else if (scripts && scripts.start) {
        return execa("npm", ["run", "start"], {
          cwd: projectPath,
          stdout: "inherit",
        });
      } else {
        log.warn("未找到启动命令");
      }
    } else {
    }
  }

那么至此我们的源码下载脚手架就已经开发完成了,最后我们用一张图总结所有流程

image.png

image.png

相关文章
|
2月前
|
前端开发 JavaScript 定位技术
一、前端高德地图注册、项目中引入、渲染标记(Marker)and覆盖物(Circle)
文章介绍了如何在前端项目中注册并使用高德地图API,包括注册高德开放平台账号、引入高德地图到项目、以及如何在地图上渲染标记(Marker)和覆盖物(Circle)。
91 1
|
1月前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
115 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
20天前
|
前端开发 Unix 测试技术
揭秘!前端大牛们如何高效管理项目,确保按时交付高质量作品!
【10月更文挑战第30天】前端开发项目涉及从需求分析到最终交付的多个环节。本文解答了如何制定合理项目计划、提高团队协作效率、确保代码质量和应对项目风险等问题,帮助你学习前端大牛们的项目管理技巧,确保按时交付高质量的作品。
31 2
|
1月前
|
资源调度 前端开发 JavaScript
前端研发链路之脚手架
本文首发于微信公众号“前端徐徐”。文章介绍了前端开发中脚手架工具的重要性及其工作原理。脚手架工具能够大幅提升开发效率,确保代码质量和项目一致性。文章详细探讨了脚手架的历史、工作原理、常见工具及其优势与潜在问题,并展望了其未来发展方向,帮助开发者更好地理解和应用脚手架工具。
42 4
前端研发链路之脚手架
|
1月前
|
前端开发 JavaScript 应用服务中间件
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
本文是一篇详细的教程,介绍了如何在Linux系统上安装和配置nginx,以及如何将打包好的前端项目(如Vue或React)上传和部署到服务器上,包括了常见的错误处理方法。
373 0
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
|
2月前
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
1月前
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
|
1月前
|
前端开发 JavaScript 开发工具
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(三)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(三)
36 0
|
1月前
|
Web App开发 前端开发 JavaScript
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
50 0
|
1月前
|
Web App开发 移动开发 前端开发
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
46 0

热门文章

最新文章

下一篇
无影云桌面