资讯专栏INFORMATION COLUMN

搭建一个通用的脚手架

187J3X1 / 3532人阅读

摘要:在年年底的时候,同事聊起脚手架。由于公司业务的多样性前端的灵活性让我们不得不思考更通用的脚手架。针对开发使用的脚手架针对项目创建项目通用脚手架是一款强壮的且有一系列工具的通用型脚手架,但发布指定名称,和用其开发工具。

在16年年底的时候,同事聊起脚手架。由于公司业务的多样性,前端的灵活性,让我们不得不思考更通用的脚手架。而不是伴随着前端技术的发展,不断的把时间花在配置上。于是chef-cli诞生了。 18年年初,把过往一年的东西整理和总结下,重新增强了原有的脚手架project-next-cli, 不单单满足我们团队的需求,也可以满足其他人的需求。

project-next-cli

面向的目标用户:

公司业务杂,但有一定的积累

爱折腾的同学和团队

借助github大量开发模板开发

发展

前端这几年(13年-15年)处于高速发展,主要表现:

备注:以下发展过程出现,请不要纠结出现顺序 [捂脸]

库/框架:jQuery, backbone, angular,react,vue

模块化:commonjs, AMD(CMD), UMD, es module

任务管理器:npm scripts, grunt, gulp

模块打包工具: r.js, webpack, rollup, browserify

css预处理器:Sass, Less, Stylus, Postcss

静态检查器:flow/typescript

测试工具:mocha,jasmine,jest,ava

代码检测工具:eslint,jslint

开发

当我们真实开发中,会遇到各种各样的业务需求(场景),根据需求和场景选用不同的技术栈,由于技术的进步和不同浏览器运行时的限制,不得不配置对应的环境等,导致我们从而满足业务需求。

画了一张图来表示,业务,配置(环境),技术之间的关系

前端配置工程师

于是明见流传了一个新的职业,前端配置工程师 O(∩_∩)O~

社区现状 专一的脚手架

社区中存在着大量的专一型框架,主要针对一个目标任务做定制。比如下列脚手架

vue-cli

vue-cli提供利用vue开发webpack, 以及 远程克隆生成文件等 pwa等模板,本文脚手架参考了vue-cli的实现。

dva-cli

dva-cli 针对dva开发使用的脚手架

think-cli

think-cli 针对 thinkjs项目创建项目

通用脚手架

yeoman

yeoman是一款强壮的且有一系列工具的通用型脚手架,但yeoman发布指定package名称,和用其开发工具。具体可点击这里查看yeoman添加生成器规则

开发初衷和目标

由于公司形态决定了,业务类型多样,前端技术发展迭代,为了跟进社区发展,更好的完成下列目标而诞生。

完成业务:专心,稳定,快速

团队规范:代码规范,测试流程,发布流程

沉淀:专人做专事,持续稳定的迭代更新,跟进时代

效益:少加班,少造轮子,完成kpi,做更有意义的事儿

实现准备

依托于Github,根据Github API来实现,如下:

获取项目

curl -i https://api.github.com/orgs/project-scaffold/repos

获取版本

curl -i https://api.github.com/repos/project-scaffold/cli/tags
实现逻辑

根据github api获取到项目列表和版本号之后,根据输入的名称,选择对应的版本下载到本地私有仓库,生成到执行目录下。核心流程图如下:。

总体设计

规范

使用Node进行脚手架开发,版本选择 >=6.0.0

选用async/await开发,解决异步回调问题

使用babel编译

使用ESLint规范代码

功能

遵守单一职责原则,每个文件为一个多带带模块,解决独立的问题。可以自由组合,从而实现复用。以下是最终的目录结构:

├── LICENSE
├── README.md
├── bin
│   └── project
├── package.json
├── src
│   ├── clear.js
│   ├── config.js
│   ├── helper
│   │   ├── metalAsk.js
│   │   ├── metalsimth.js
│   │   └── render.js
│   ├── index.js
│   ├── init.js
│   ├── install.js
│   ├── list.js
│   ├── project.js
│   ├── search.js
│   ├── uninstall.js
│   ├── update.js
│   └── utils
│       ├── betterRequire.js
│       ├── check.js
│       ├── copy.js
│       ├── defs.js
│       ├── git.js
│       ├── loading.js
│       └── rc.js
└── yarn.lock
配置和主框架 使用babel-preset-env保证版本兼容
{
  "presets": [
    ["env", {
      "targets": {
        "node": "6.0.0"
      }
    }]
  ]
}
使用eslint管理代码

eslint demo

{
  "parserOptions": {
      "ecmaVersion": 7,
      "sourceType": "module",
      "ecmaFeatures": {
          "jsx": true
      }
  },
  "extends": "airbnb-base/legacy",
  "rules": {
      "consistent-return": 1,
      "prefer-destructuring": 0,
      "no-mixed-spaces-and-tabs": 0,
      "no-console": 0,
      "no-tabs": 0,
      "one-var":0,
      "no-unused-vars": 2,
      "no-multi-spaces": 2,
      "key-spacing": [
        2,
        {
          "beforeColon": false,
          "afterColon": true,
          "align": {
            "on": "colon"
          }
        }
      ],
      "no-return-await": 0
  },
  "env": {
      "node": true,
      "es6": true
  }
}
使用husky检测提交

使用husky, 来定义git-hooks, 规范git代码提交流程,这里只做 commit校验

package.json配置如下:

"husky": {
    "hooks": {
      "pre-commit": "npm run lint"
    }
}
入口

统一配置和入口,分发到不同单一文件,执行输出。核心代码

function registerAction(command, type, typeMap) {
  command
    .command(type)
    .description(typeMap[type].desc)
    .alias(typeMap[type].alias)
    .action(async () => {
      try {
        if (type === "help") {
          help();
        } else if (type === "config") {
          await project("config", ...process.argv.slice(3));
        } else {
          await project(type);
        }
      } catch (e) {
        console.log(e);
        help();
      }
    });

  return command;
}
本地配置读和写

配置用来获取脚手架的基本设置, 如registry, type等基本信息。

使用

project config set registry koajs # 设置本地仓库下载源

project config get registry # 获取本地仓库设置的属性

project config delete registry # 删除本地设置的属性

逻辑

判定本地设置文件存在 ===> 读/写

本地配置文件, 格式是 .ini
若中间每一步 数据为空/文件不存在 则给予提示

核心代码

switch (action) {
    case "get":
      console.log(await rc(k));
      console.log("");
      return true;

    case "set":
      await rc(k, v);
      return true;

    case "remove":
      await rc(k, v, true);
      return true;

    default:
      console.log(await rc());

下面每个命令的实现逻辑。

下载

使用

project i

逻辑

Github API ===> 获取项目列表 ===> 选择一个项目 ===> 获取项目版本号 ===> 选择一个版本号 ===> 下载到本地仓库

获取项目列表

https://api.github.com/orgs/p...

获取tag列表

若中间每一步 数据为空/文件不存在 则给予提示

请求代码

request

function fetch(api) {
  return new Promise((resolve, reject) => {
    request({
      url    : api,
      method : "GET",
      headers: {
        "User-Agent": `${ua}`
      }
    }, (err, res, body) => {
      if (err) {
        reject(err);
        return;
      }

      const data = JSON.parse(body);
      if (data.message === "Not Found") {
        reject(new Error(`${api} is not found`));
      } else {
        resolve(data);
      }
    });
  });
}

下载代码

download-git-repo

export const download = async (repo) => {
  const { url, scaffold } = await getGitInfo(repo);

  return new Promise((resolve, reject) => {
    downloadGit(url, `${dirs.download}/${scaffold}`, (err) => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
};

核心代码

  // 获取github项目列表
  const repos = await repoList();

  choices = repos.map(({ name }) => name);
  answers = await inquirer.prompt([
    {
      type   : "list",
      name   : "repo",
      message: "which repo do you want to install?",
      choices
    }
  ]);
  // 选择的项目
  const repo = answers.repo;

  // 项目的版本号劣币爱哦
  const tags = await tagList(repo);

  if (tags.length === 0) {
    version = "";
  } else {
    choices = tags.map(({ name }) => name);

    answers = await inquirer.prompt([
      {
        type   : "list",
        name   : "version",
        message: "which version do you want to install?",
        choices
      }
    ]);
    version = answers.version;
  }
  // 下载
  await download([repo, version].join("@"));
生成项目

使用

project init

逻辑

获取本地仓库列表 ===> 选择一个本地项目 ===> 输入基本信息 ===> 编译生成到临时文件 ===> 复制并重名到目标目录

若中间每一步 数据为空/文件不存在/生成目录已重复 则给予提示

核心代码

  // 获取本地仓库项目
  const list = await readdir(dirs.download);

  // 基本信息
  const answers = await inquirer.prompt([
    {
      type   : "list",
      name   : "scaffold",
      message: "which scaffold do you want to init?",
      choices: list
    }, {
      type   : "input",
      name   : "dir",
      message: "project name",
      // 必要的验证
      async validate(input) {
        const done = this.async();

        if (input.length === 0) {
          done("You must input project name");
          return;
        }

        const dir = resolve(process.cwd(), input);

        if (await exists(dir)) {
          done("The project name is already existed. Please change another name");
        }

        done(null, true);
      }
    }
  ]);
  const metalsmith = await rc("metalsmith");
  if (metalsmith) {
    const tmp = `${dirs.tmp}/${answers.scaffold}`;
    // 复制一份到临时目录,在临时目录编译生成
    await copy(`${dirs.download}/${answers.scaffold}`, tmp);
    await metal(answers.scaffold);
    await copy(`${tmp}/${dirs.metalsmith}`, answers.dir);
    // 删除临时目录
    await rmfr(tmp);
  } else {
    await copy(`${dirs.download}/${answers.scaffold}`, answers.dir);
  }

其中模板引擎编译实现核心代码如下:

// metalsmith逻辑
function metal(answers, tmpBuildDir) {
    return new Promise((resolve, reject) => {
    metalsmith
      .metadata(answers)
      .source("./")
      .destination(tmpBuildDir)
      .clean(false)
      .use(render())
      .build((err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(true);
      });
  });
}
// metalsmith render中间件实现
function render() {
    return function _render(files, metalsmith, next) {
    const meta = metalsmith.metadata();

    /* eslint-disable */
    
    Object.keys(files).forEach(function(file){
      const str = files[file].contents.toString();

      consolidate.swig.render(str, meta, (err, res) => {
        if (err) {
          return next(err);
        }

        files[file].contents = new Buffer(res);
        next();
      });
    })
    
  }
}
升级/降级版本

使用

project update

逻辑

获取本地仓库列表 ===> 选择一个本地项目 ===> 获取版本信息列表 ===> 选择一个版本 ===> 覆盖原有的版本文件

若中间每一步 数据为空/文件不存在 则给予提示

核心代码

  // 获取本地仓库列表
  const list = await readdir(dirs.download);

  // 选择一个要升级的项目
  answers = await inquirer.prompt([
    {
      type   : "list",
      name   : "scaffold",
      message: "which scaffold do you want to update?",
      choices: list,
      async validate(input) {
        const done = this.async();

        if (input.length === 0) {
          done("You must choice one scaffold to update the version. If not update, Ctrl+C");
          return;
        }

        done(null, true);
      }
    }
  ]);

  const repo = answers.scaffold;

  // 获取该项目的版本信息
  const tags = await tagList(repo);

  if (tags.length === 0) {
    version = "";
  } else {
    choices = tags.map(({ name }) => name);

    answers = await inquirer.prompt([
      {
        type   : "list",
        name   : "version",
        message: "which version do you want to install?",
        choices
      }
    ]);
    version = answers.version;
  }
  // 下载覆盖文件
  await download([repo, version].join("@"))
搜索

搜索远程的github仓库有哪些项目列表

使用

project search

逻辑

获取github项目列表 ===> 输入搜索的内容 ===> 返回匹配的列表

若中间每一步 数据为空 则给予提示

核心代码

 const answers = await inquirer.prompt([
    {
      type   : "input",
      name   : "search",
      message: "search repo"
    }
  ]);

  if (answers.search) {
    let list = await searchList();

    list = list
      .filter(item => item.name.indexOf(answers.search) > -1)
      .map(({ name }) => name);

    console.log("");
      if (list.length === 0) {
          console.log(`${answers.search} is not found`);
      }
      console.log(list.join("
"));
      console.log("");
  }
总结

以上是这款通用脚手架产生的背景,针对用户以及具体实现,该脚手架目前还有一些可以优化的地方:

不同源,存储不同的文件

支持离线功能

硬广:如果您觉得project-next-cli好用,欢迎star,也欢迎fork一块维护。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101986.html

相关文章

  • webpack4详细教程,从无到有搭建react手架(一)

    摘要:是一个现代应用程序的静态模块打包器,前端模块化的基础。作为一个前端工程师切图仔,非常有必要学习。官网的文档非常的棒,中文文档也非常给力,可以媲美的文档。建议先看概念篇章,再看指南,然后看和配置总览。 webpack 是一个现代 JavaScript 应用程序的静态模块打包器,前端模块化的基础。作为一个前端工程师(切图仔),非常有必要学习。 showImg(https://segment...

    zhkai 评论0 收藏0
  • 使用webpack从0搭建多入口网站手架,可复用导航栏/底部通栏/侧边栏,根据页面文件自动更改配置

    摘要:官方推荐不写重复的配置,即把本地和生产环境共用的配置放到一个文件,然后通过进行合并我们可以看到,通过插件,将共用配置和开发的配置进行合并定义了全局变量这个插件是为了在我们允许后,自动打开页面,避免每次都手动打开。 之前只知道webpack很强大,但是一直没有深入学习过,这次从头看了一下教程,然后从0开始搭建了一个多入口网站的开发脚手架,期间遇到过很多问题,所以有心整理一下,希望能给大家...

    isLishude 评论0 收藏0
  • 使用webpack从0搭建多入口网站手架,可复用导航栏/底部通栏/侧边栏,根据页面文件自动更改配置

    摘要:官方推荐不写重复的配置,即把本地和生产环境共用的配置放到一个文件,然后通过进行合并我们可以看到,通过插件,将共用配置和开发的配置进行合并定义了全局变量这个插件是为了在我们允许后,自动打开页面,避免每次都手动打开。 之前只知道webpack很强大,但是一直没有深入学习过,这次从头看了一下教程,然后从0开始搭建了一个多入口网站的开发脚手架,期间遇到过很多问题,所以有心整理一下,希望能给大家...

    jaysun 评论0 收藏0
  • vue和react差异

    摘要:而中实现原理是利用高阶函数通过将多个函数组合成一个可执行执行函数关键步骤代码如下所示。和都是基于更新差异元素。 引言 平时开发单页项目应用基于vue,目前另外两个比较热的库还有angular和react,angular 1系列用过,进入公司后由于基于vue技术栈就没在关注了。一直在关注react,目的不是学习用法,只是为了拓展自己的视野和思维,通过了解一些使用上的差异性,来进一步的思考...

    OnlyLing 评论0 收藏0
  • 如何构建大型前端项目

    摘要:如何构建大型的前端项目搭建好项目的脚手架一般新开发一个项目时,我们会首先搭建好一个脚手架,然后才会开始写代码。组件化一般分为项目内的组件化和项目外的组件化。 如何构建大型的前端项目 1. 搭建好项目的脚手架 一般新开发一个项目时,我们会首先搭建好一个脚手架,然后才会开始写代码。一般脚手架都应当有以下的几个功能: 自动化构建代码,比如打包、压缩、上传等功能 本地开发与调试,并有热替换与...

    lykops 评论0 收藏0

发表评论

0条评论

187J3X1

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<