chrome扩展训练营 | 5. chrome扩展脚手架实现

1,180 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 2 天,点击查看活动详情

王志远,微医前端技术部

系列文目录

小节主题文档期待产出补充
学习起始篇juejin.cn/post/714860…扩展能做到什么/如何学习扩展/扩展基础组成概念
开发调试发布juejin.cn/post/714861…熟悉谷歌扩展的开发流程/调试流程/发布流程
谷歌扩展开发能力详解:数据流处理能力、UI 能力、浏览器特性(历史/书签/下载/网络请求/等等)juejin.cn/post/714710…扩展能力知识体系学习,根据文档实现所有相关 demo
框架升级:Vue 开发实现一图一诗(新开 tab 页打开自己的内容)juejin.cn/post/715234…使用 Vue 开发扩展
chrome 扩展脚手架实现:vue2+elementui 版本本文以【vue-chrome-cli】为例掌握脚手架的实现
落地:debug-plugin 集成谷歌扩展原调试插件重构为谷歌扩展并集成进项目

前言

​ 在上篇文章框架升级:Vue 开发实现一图一诗(新开 tab 页打开自己的内容)中,我们实现了 vue 开发 chrome 扩展到模版项目,并使用其二次开发了一个【一图一诗】扩展; ​ 文章中提到的脚手架帮我们解决了需要一直复制粘贴项目或者手动更改常规配置的烦恼,最关键的是,给我们提供了更大的优化空间,比如【实现新建脚手架时选择 background/popup 功能】/【支持网络请求配置】/【支持 vue3/ts/react 模版】等等的可能性; ​ 是不是很酷?想不想学? 在本文中,我们将通过 vue-chrome-cli 的学习,收获:如何实现一个脚手架

实现思路

创建一个脚手架,本质上就是实现一个全局包,只不过其职能是根据配置项创建项目

  1. 搭建全局包:配置 package.json 即可
  2. 实现脚手架逻辑
    1. 配置可执行命令 可以基于commander
    2. 要实现脚手架 先做一个命令行交互的功能 可以基于inquirer
    3. 项目模板下载 可以基于 download-git-repo
    4. 根据用户的选择动态的生成内容 可以基于metalsmith
  3. 发布全局包:
    1. 包的发布流程:注册 - 登录 - 源切换 (npm 源管理工具 nrm)- 发布
    2. 版本自动更新:npm version
    3. 简化发布,一步完成【git 提交 + npm 版本更新 + npm 发布】:发布的 shell 脚本

具体实现

接下来,我们以【vue-chrome-cli】脚手架的实现为例,来实现上面的思路

【表情】

搭建全局包

  • 项目搭建
  • 配置修改
  • 入口搭建
  • 执行测试
  • 支持调试

项目搭建

找一个空目录,先初始化一个 npm 项目

npm init -y

配置修改

然后对 package.json 进行修改

  • 指定全局命令入口文件:bin 字段
  • 指定发包作者:author 字段
"bin": {
	"vue-chrome-cli": "main.js"
},
"author": "wzyuan",

入口搭建

在项目根目录下创建 main.js,并指明运行环境是 node

main.js

#! /usr/bin/env node

console.log("starting...");

此时项目结构如下

.
|-- main.js
|-- package-lock.json
|-- package.json

执行测试

那让我们来试用下叭!对于本地包我们该怎么去调试呢?npm 提供了 link 命令,将包链接到全局下,以直接用命令方式执行

npm link

此时 这个包就变成了全局包,执行命令为name属性对应的值,执行时会默认执行指定的脚本文件;效果如下

是不是很赞?

支持调试

这里补充一下,如果我们这么去执行是没法 debug 的,一遍遍的 log 太不友好了,那有没有更好的方案呢?vscode 给我们提供了可视化 debug 的能力,我在此分享一下。

配置 vscode 的 launch.json 文件(这里的意思是执行当前打开的文件并传递对应 args 参数)

{
  "version": "1.0.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "skipFiles": ["<node_internals>/**"],
      "program": "${file}",
      "args": ["init", "projectName"], // 命令参数
      "console": "integratedTerminal" // 不使用默认值 internalTerminal。
      //使用 integratedTerminal vscode 集成的终端,externalTerminal 外部终端都是可以的。
    }
  ]
}

打开入口文件并执行图形化调试

可以发现我们已经在断点处停住了。

实现脚手架逻辑

我们来整理下思路,当命令被触发时我们需要监听并执行后续逻辑,后续逻辑中我们需要收集用户的定制信息,下载对应模版仓库并根据收集到的信息进行改造,就完成了脚手架的核心了。

不重复造轮子,基于此,我们可以采用如下依赖分别实现

  1. 监听并执行,配置可执行命令 可以基于commander
  2. 收集用户的定制信息,做一个命令行交互的功能 可以基于inquirer
  3. 项目模板下载 可以基于 download-git-repo
  4. 根据用户的选择动态的生成内容 可以基于metalsmith

这些依赖不一一赘述了,见使用

我们先安装下依赖,这里贴下依赖版本避免版本错误

{
  "dependencies": {
    "chalk": "^2.4.2",
    "commander": "^3.0.2",
    "download-git-repo": "^2.0.0",
    "inquirer": "^7.0.0",
    "ora": "^4.0.2"
  }
}

我们先实现一个 log 类用于打印,这里采用 chalk 美化

utils/log.js

const chalk = require("chalk");

module.exports = class Log {
  constructor() {
    this.log = console.log;
  }

  error(msg) {
    if (typeof msg === "object") {
      msg = JSON.stringify(msg);
    }
    this.log(chalk.red(msg));
  }

  warning(msg) {
    if (typeof msg === "object") {
      msg = JSON.stringify(msg);
    }
    this.log(chalk.yellow(msg));
  }

  success(msg) {
    if (typeof msg === "object") {
      msg = JSON.stringify(msg);
    }
    this.log(chalk.green(msg));
  }

  clearLog() {
    process.stdout.write(
      process.platform === "win32" ? "\x1Bc" : "\x1B[2J\x1B[3J\x1B[H"
    );
  }
};

配置可执行命令

在执行脚本 main 中实现逻辑

#! /usr/bin/env node

const cmd = require("commander");
cmd
  .command("init")
  .description("初始化模板")
  .action(async (args) => {
    typeof args !== "string" &&
      (console.log(log.error("缺少必填参数")), process.exit(1));
    try {
      console.log("init callback");
      process.exit(0);
    } catch (error) {
      process.exit(1);
    }
  });
cmd.parse(process.argv);

测试效果如下

命令行交互

架子已经搭建起来了,其实我们这时候就是在不同的命令下去执行不同的逻辑处理,中间如果存在交互式配置,比如新建项目时目标文件路径已经存在,则需问询用户是否进行覆盖,则可以使用inquier

这里我们抽离一个 src/cli.js 文件,专门用于收集用户信息

src/cli.js

const inquirer = require("inquirer");
const OPTIONS = [
  {
    name: "type",
    default: "vue2",
    message: "你想创建一个什么类型的模板(vue2|vue3|react|ssr)",
  },
  {
    name: "projectName",
    default: "my-chrome-extension",
    message: "请输入你的插件名",
  },
  {
    name: "ui",
    default: "elementui",
    message: "采用哪个 ui 框架",
  },
];

module.exports = () => {
  return inquirer.prompt(OPTIONS);
};

然后在入口文件中引入执行

#! /usr/bin/env node

const cmd = require("commander");
const options = require("./src/cli");
const Log = require("./utils/log");
const log = new Log();

cmd
  .command("init")
  .description("初始化模板")
  .action(async (args) => {
    try {
      console.log("init callback");
      log.warning("vue 开发 chrome 脚手架初始化模板 \n");
      // 填选项
      let chooses = await options();
      log.success(chooses);
      process.exit(0);
    } catch (error) {
      process.exit(1);
    }
  });
cmd.parse(process.argv);

测试效果如下

项目模板下载

承接上文,当我们收集好用户的定制化信息之后,我们很自然能想到,目前只需要实现下载模板的逻辑就可以了。

这里我们实现一个工具文件,包含功能

  • 安装依赖
  • 执行 shell 命令
  • 安装指定仓库

utils/index.js

const spawn = require("child_process").spawn;
// git 包
const downLoad = require("download-git-repo");
// 动画
const ora = require("ora");

const Log = require("./log");

const log = new Log();

/**
 * Runs `npm install` in the project directory
 * @param {string} cwd Path of the created project directory
 * @param {object} data Data from questionnaire
 */
exports.installDependencies = function installDependencies(
  cwd,
  executable = "npm"
) {
  log.success("# Installing project dependencies ...");
  console.log("# ========================\n");
  return runCommand(executable, ["install"], {
    cwd,
  });
};

/**
 * Spans a child process and runs the specified command
 * By default, runs in the CWD and inherits stdio
 * Options are the same as node's child_process.spawn
 * @param {string} cmd
 * @param {array<string>} args
 * @param {object} options
 */
function runCommand(cmd, args, options) {
  return new Promise((resolve, reject) => {
    const spwan = spawn(
      cmd,
      args,
      Object.assign(
        {
          cwd: process.cwd(),
          stdio: "inherit",
          shell: true,
        },
        options
      )
    );

    spwan.on("exit", () => {
      resolve();
    });
  });
}
/** 下载 github 仓库
 *
 * @param {*} url 仓库相对路径 用户名/仓库名
 * @param {*} name 下拉之后的目录名
 * @param {*} options 下拉配置
 * @returns
 */
exports.downGit = (
  url,
  name,
  options = {
    clone: false,
  }
) => {
  const spinner = ora("正在拉取模板...");
  spinner.start();
  return new Promise((resolve, reject) => {
    downLoad(url, name, options, (err) => {
      spinner.stop();
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
};

在这我们指定模版仓库地址为Sympath/vue-chrome-template,在入口文件中使用逻辑如下

main.js

#! /usr/bin/env node

const cmd = require("commander");
const options = require("./src/cli");
const Log = require("./utils/log");
const log = new Log();
const { downGit, installDependencies } = require("./utils/index");
let repoUrl = "Sympath/vue-chrome-template";
cmd
  .command("init")
  .description("初始化模板")
  .action(async (args) => {
    try {
      console.log("init callback");
      log.warning("vue 开发 chrome 脚手架初始化模板 \n");
      // 填选项
      let chooses = await options();
      try {
        // 拉取
        await downGit(repoUrl, chooses.projectName);
        log.success("项目创建成功\n");
      } catch (error) {
        log.error("项目创建失败 原因:\n", error);
      }
      process.exit(0);
    } catch (error) {
      process.exit(1);
    }
  });
cmd.parse(process.argv);

测试效果如下

发包

  1. 包的发布流程
  2. 版本自动更新:npm version
  3. 简化发布,一步完成【git 提交 + npm 版本更新 + npm 发布】:发布的 shell 脚本

包的发布流程

一、在 npm 官网注册账户:npm 官网

二、在填写的邮箱中验证邮箱:如果注册之后没有验证邮箱,那么发布的时候会报错

如果仓库第一次提交,名称被占用了也会出现 403,需注意

三、在命令行中登录:npm login

四、使用 npm 源:可以直接用原生npm config set registry=http://registry.npmjs.org,也可以使用 npm 源管理工具 nrm,先安装npm i nrm -g;然后nrm use npm

五、进入需要发布的目录发布项目

六、修改更新版本:如果要更新版本,需要先 git 上面提交代码,并且修改package.json里面的"version"字段提升,否则无法再次提交

版本自动更新:npm version

关于第六点版本更新,我们每次手动更新难免出错,并且直接改 package.json 也很奇怪,这里我们可以使用npm version命令,version 是一串由两个点分割的三段数字,不同数字的提升对应着不同的参数

npm version xxx
patch 第三段数字
minor 第二段数字
major 第一段数字

发布的 shell 脚本

想想我们在更新一个新特性时都要干啥?git 提交 + npm 版本更新 + npm 发布,一次两次没关系,但次数多了还是有点儿烦的,所以我们可以写一个发布脚本

sh/publish.sh

#!/bin/zsh
echo "$1"
# 如果没有输入 commit 信息就中止发布
if [[ -z "${1}" ]]; then
    echo "请输入本次修改 commit 信息"
    exit 1
fi
# 进行 git 仓库的上传
git add .
git commit -m "$1"
git push
# 更新包 minor major
npm version patch
npm publish

然后在 package.json 中配置对应命令

{
  "scripts": {
    "publish-sh": "chmod 777 ./sh/publish.sh && sh ./sh/publish.sh"
  }
}

后续我们的更新只需要执行如下一行命令即可(补充:这里用 publish-sh 的原因是和原生的 npm publish 冲突了)

npm run publish-sh "提交的 commit 信息"

总结

至此,我们就完成了一个vue-chrome-cli脚手架啦,为了便于理解,我省去了很多细节的优化,可以看到,其实也就是包的使用+思路的梳理。

相关链接如下