开发Node CLI构建微信小程序脚手架的示例


Posted in Javascript onMarch 27, 2020

本文介绍了 Node CLI 构建微信小程序脚手架的示例,分享给大家,具体如下:

开发Node CLI构建微信小程序脚手架的示例 

开发Node CLI构建微信小程序脚手架的示例 

目的

由于目前公司的 TOC 产品只要是微信小程序,而且随着业务的扩展, 会有更多的需求,创建更多的小程序,为了让团队避免每次开发前花费大量时间做比如工程化的一些配置,以及保持每个项目的一致性, 所以决定做一个 Node CLI 来创建微信小程序脚手架

  • 节省开发前期的大量时间,新项目可以很快开始业务开发
  • 保证项目统一性,有利于团队间的协作及工程化
  • 提升团队基建意识,从枯燥无味的业务开发中脱离出来,尝试新的东西,即使很基础很简单

小程序选型

小程序的第三方框架有很多, 我接触过的就有 taro / wepy / mpvue ,并且都有对应上线的项目。 在尝试这些框架的过程中,对比原生小程序,有一些感想想分享出来:

  • 第三方框架语法贴近vue/react, 开发者可以根据自己的特点选择框架,学习成本相对较低
  • 原生框架在CSS预处理,多端复用,状态管理,自动构建这几块能力对比其他框架是欠缺的
  • 第三方框架额外的工具包会使打包体积变大,每次构建花费时间,同时性能不如原生
  • 第三方框架更新迭代很快,比如wepy@1.x/wepy@2, 导致旧项目的更新问题
  • 小程序的特性更新迭代速度较快, 第三方框架会相对滞后

综上所述,由于我们目前没有多端复用的要求,并且有的小程序相对简单,需要很短时间内开发完成, 最重要的是,其他的框架我都试过了,原生的还没写过,一个字,新鲜感!!:smile: ,所以最终当仁不让地选择了原生小程序,不得不说,原生大法就是妙啊! :clap::clap::clap::clap:

大体思路

这个功能是相对很基础的,但是作为一个每天搬砖的业务仔来说,是个艰难的过程,也是个很好的学习机会。

在做之前,想找找个社区比较:ox::beer:的学习(抄)一下,短暂考虑后,果断选择 taro-cli , 然后火速打开源码,一顿操作(完全蒙圈),学习了一点之后,才开始上手

这个具体的实现思路我想到两个

  • git clone 远程仓库作为模版下载到本地,再根据用户输入配置修改 .json 文件(比如 appId )
  • template 就放在当前目录中,直接`copy``, 之后的事等同

权衡之后,打算使用 lerna 作为管理工具, 其中模版也作为一个 npm 包 ,用到的时候去 npm 下载,这么做我是为了方便管理,统一 push / publish , 就是为了省事 :smile:。

最终思路:

暴露命令 —> 用户交互输入配置 -> 集合配置下载模版 -> 根据配置修改 .json -> git init + 安装依赖

开发 Node CLI

Lerna 项目搭建

知道 monorepo 的同学不需要我多说,其实就是把代码放在一个仓库里,结果包之间回想以来,发布繁琐等问题, 这里我们就用到了 lerna 这个神器帮助我们做包的统一管理

// 创建项目
mkdir modoo-mini-program
cd modoo-mini-program

// 初始化
lerna init

cd packages
mkdir modoo-script
mkdir modoo-template-mini
mkdir modoo-mini // 安装 modoo-script 依赖用于测试,无其他实际用处

lerna bootstrap // 安装依赖 + npm link

安装依赖

为了实现功能,我们需要安装一些依赖包

  • commander 命令行工具,用于读取命令参数,作对应操作
  • node-fs-extra 在 Node.js 的 fs 基础上增加了一些新的方法,更好用,还可以拷贝模板。
  • chalk 可以用于控制终端输出字符串的样式, 调整颜色啥的
  • inquirer 用户命令行交互,获取用户的交互配置数据,就像个提问板
  • ora 实现加载中的状态是一个 Loading 加前面转起来的小圈圈,成功了是一个 Success 加前面一个小钩钩。
  • log-symbols 日志彩色符号,用来显示√ 或 × 等的图标

获取命令

首先第一步,要在用户全局安装之后,暴露出命令接口,需要在 packages.json 文件中加入如下内容

"bin": {
  "modoo-script": "./bin/modoo-script.js"
},

之后在根目录下创建 bin 文件夹 + bin/modoo-script.js

#!/usr/bin/env node
const { program } = require("commander");

program
 .version(require("../package").version) // modoo-script --version
 .usage("<command> [options]")
 // init 命令,床架项目
 .command("init [projectName]", "Init a project with default templete")
 .parse(process.argv); // 解析命令参数

然后需要注意的是, commander 支持 Git 风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是 [command]-[subcommand] ,例如:

modoo-script init => modoo-script-init
modoo-script build => modoo-script-build

所以为了实现 init 命令,可以直接在 bin 文件目录下添加 modoo-script-init.js

#!/usr/bin/env node

const { program } = require("commander");

program
 .option("--name [name]", "项目名称")
 .option("--description [description]", "项目介绍")
 .option("--framework", "脚手架框架")
 .parse(process.argv);

const args = program.args;
// 获取命令参数
const { name, description, framework } = program;

const projectName = args[0] || name;

......

用户交互

获取了命令参数后,根据参数转到用户交互界面,这里使用的是 inquirer 来处理命令行交互, 用法很简单

const inquirer = require('inquirer')

if (typeof conf.description !== 'string') {
   prompts.push({
    type: 'input',
    name: 'description',
    message: '请输入项目介绍!'
   })
}

......

inquirer.prompt(prompts).then(answers => {
  // 整合配置
  this.conf = Object.assign(this.conf, answers);
})

远程模块

这里较为折腾,一开始说了,我把模版作为 npm包 ,具体查找,下载的过程如下

  • npm search 查找相应的模版 npm 包
  • 在用户选择框架后对应所需的包,获取它的详细信息,主要是 tarball
  • 用户输入完后,下载 tarball 到项目目录,并修改 .json 文件配置

部分代码如图所示

// 一 npm search 查找相应的模版 npm 包
const { execSync } = require("child_process");

module.exports = () => {
 let list = [];
 try {
  const listJSON = execSync(
   "npm search --json --registry http://registry.npmjs.org/ @modoo/modoo-template"
  );
  list = JSON.parse(listJSON);
 } catch (error) {}

 return Promise.resolve(list);
};
// 二 返回 npm 数据
const pkg = require("package-json");
const chalk = require("chalk");
const logSymbols = require("log-symbols");

exports.getBoilerplateMeta = framework => {
 log(
  logSymbols.info,
  chalk.cyan(`您已选择 ${framework} 远程模版, 正在查询该模版...`)
 );

 return pkg(framework, {
  fullMetadata: true
 }).then(metadata => {
  const {
   dist: { tarball },
   version,
   name,
   keywords
  } = metadata;
  log(
   logSymbols.success,
   chalk.green(`已为您找到 ${framework} 远程模版, 请输入配置信息`)
  );

  return {
   tarball,
   version,
   keywords,
   name
  };
 });
};
// 三 下载 npm 包
const got = require("got");
const tar = require("tar");
const ora = require("ora");

const spinner = ora(
  chalk.cyan(`正在下载 ${framework} 远程模板仓库...`)
).start();

const stream = await got.stream(tarball);

fs.mkdirSync(proPath);

const tarOpts = {
 strip: 1,
 C: proPath
};

// 管道流传输下载文件到当前目录
stream.pipe(tar.x(tarOpts)).on("close", () => {
  spinner.succeed(chalk.green("下载远程模块完成!"));
  ......
})
// 四 遍历文件修改配置
const fs = require("fs-extra");

readFiles(
  proPath,
  {
   ignore: [
    ".{pandora,git,idea,vscode,DS_Store}/**/*",
    "{scripts,dist,node_modules}/**/*",
    "**/*.{png,jpg,jpeg,gif,bmp,webp}"
   ],
   gitignore: true
  },
  ({ path, content }) => {
   fs.createWriteStream(path).end(template(content, inject));
  }
 );
 
// 递归读文件
exports.readFiles = (dir, options, done) => {
 if (!fs.existsSync(dir)) {
  throw new Error(`The file ${dir} does not exist.`);
 }
 if (typeof options === "function") {
  done = options;
  options = {};
 }
 options = Object.assign(
  {},
  {
   cwd: dir,
   dot: true,
   absolute: true,
   onlyFiles: true
  },
  options
 );

 const files = globby.sync("**/**", options);
 files.forEach(file => {
  done({
   path: file,
   content: fs.readFileSync(file, { encoding: "utf8" })
  });
 });
};

// 配置替换
exports.template = (content = "", inject) => {
 return content.replace(/@{([^}]+)}/gi, (m, key) => {
  return inject[key.trim()];
 });
};

下载依赖

下载完毕并且修改完配置后, 默认执行 git init + 根据环境( yarn / npm / cnpm )安装依赖,这个就很简单了

const { exec } = require("child_process");
const ora = require("ora");
const chalk = require("chalk");

// proPath 项目目录
process.chdir(proPath);

// git init
const gitInitSpinner = ora(
 `cd ${chalk.cyan.bold(projectName)}, 执行 ${chalk.cyan.bold("git init")}`
).start();

const gitInit = exec("git init");
gitInit.on("close", code => {
 if (code === 0) {
  gitInitSpinner.color = "green";
  gitInitSpinner.succeed(gitInit.stdout.read());
 } else {
  gitInitSpinner.color = "red";
  gitInitSpinner.fail(gitInit.stderr.read());
 }
});

// install
let command = "";
if (shouldUseYarn()) {
 command = "yarn";
} else if (shouldUseCnpm()) {
 command = "cnpm install";
} else {
 command = "npm install";
}

log(" ".padEnd(2, "\n"));
const installSpinner = ora(
 `执行安装项目依赖 ${chalk.cyan.bold(command)}, 需要一会儿...`
).start();

exec(command, (error, stdout, stderr) => {
  if (error) {
   installSpinner.color = "red";
   installSpinner.fail(chalk.red("安装项目依赖失败,请自行重新安装!"));
   console.log(error);
  } else {
   installSpinner.color = "green";
   installSpinner.succeed("安装成功");
   log(`${stderr}${stdout}`);
  }
});

主要的代码就是这些,其实只要知道思路,这些东西都很简单,虽然我写的有点 ️:chicken:,但是主要的逻辑还是能理清楚的一些的。更加详细的可以去:eyes:我发的源码,多谢指教。:pray::pray::pray:

开发脚手架

因为这是小程序的脚手架,它不像其他 web 框架一样需要很多 webpack 的配置,所以相对简单很多。

对于这个脚手架,相比于开发者工具创建的默认项目,我弥补了它的一些问题

  1. 默认项目太过简单,只适合自己折腾,对于团队或者企业,缺乏相应的代码约定/规范,没有强制的约定会导致团队协作间的困难,提升code review的难度,所以我在原来的基础上加入了eslint,stylelint,prettier,commitlint等配置,以及git hook 在 pre-commit 时,执行校验,确保提交的代码尽量规范
  2. 由于对 css 预处理的钟爱,另外加入了对 less 的支持,并且解决小程序背景图不支持本地图片的问题
  3. 由于以上基本都是文件处理,所以选择 gulp 作为构建工具,这里是 v4, 与v3 写法上有一定的区别,不过关系不大

在根目录下创建 gulpfile.js

const gulp = require('gulp');
const chalk = require('chalk');
const rename = require('gulp-rename');

// 支持 less
gulp.task('less', () => {
 return gulp
  .src('./miniprogram/**/*.less')
  .pipe(less())
  .pipe(postcss()) // 配置在 post.config.js
  .pipe(
   rename((path) => {
    path.extname = '.wxss';
   })
  )
  .pipe(
   gulp.dest((file) => {
    return file.base; // 原目录
   })
  );
});

// 开发环境监听 less
if (env === 'development') {
 gulp.watch(['./miniprogram/**/*.less'], gulp.series('less')).on('change', (path) => {
  log(chalk.greenBright(`File ${path} was changed`));
 });
}


// 一下代码注释掉了,依赖包下载太慢了,这主要负责图片的压缩
const imagemin = require('gulp-imagemin');
const cache = require('gulp-cache'); // 使用缓存

gulp.task('miniimage', () => {
 return gulp
  .src('./miniprogram/**/*.{png,jpe?g,gif,svg}')
  .pipe(
   cache(
    imagemin([
     imagemin.gifsicle({ interlaced: true }),
     imagemin.mozjpeg({ quality: 75, progressive: true }),
     imagemin.optipng({ optimizationLevel: 5 }),
     imagemin.svgo({
      plugins: [{ removeViewBox: true }, { cleanupIDs: false }],
     }),
    ])
   )
  )
  .pipe(
   gulp.dest((file) => {
    return file.base; // 原目录
   })
  );
});

其他的一些具体配置,可以看我的GitHub 仓库源码

参考

taro-cli

pandora-cli

little-bird-cli

到此这篇关于开发Node CLI构建微信小程序脚手架的示例的文章就介绍到这了,更多相关Node CLI构建小程序脚手架内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
用prototype实现的简单小巧的多级联动菜单
Mar 24 Javascript
js对象的比较
Feb 26 Javascript
在jQuery中 关于json空对象筛选替换
Apr 15 Javascript
javascript中AJAX用法实例分析
Jan 30 Javascript
jQuery实现表格展开与折叠的方法
May 04 Javascript
Nginx上传文件全部缓存解决方案
Aug 17 Javascript
js获取地址栏中传递的参数(两种方法)
Feb 08 Javascript
jQuery+C#实现参数RSA加密传输功能【附jsencrypt.js下载】
Jun 26 jQuery
js数组实现权重概率分配
Sep 12 Javascript
使用JavaScript实现在页面中显示距离2017年中秋节的天数
Sep 26 Javascript
node省市区三级数据性能测评实例分析
Nov 06 Javascript
在JavaScript中查找字符串中最长单词的三种方法(推荐)
Jan 18 Javascript
微信小程序间使用navigator跳转传值问题实例分析
Mar 27 #Javascript
vue跳转页面的几种方法(推荐)
Mar 26 #Javascript
Vue项目页面跳转时浏览器窗口上方显示进度条功能
Mar 26 #Javascript
JavaScript定时器使用方法详解
Mar 26 #Javascript
js实现时钟定时器
Mar 26 #Javascript
如何解决vue在ios微信&quot;复制链接&quot;功能问题
Mar 26 #Javascript
原生JS实现留言板
Mar 26 #Javascript
You might like
php快速url重写更新版[需php 5.30以上]
2010/04/25 PHP
php+highchats生成动态统计图
2014/05/21 PHP
PHP生成图像验证码的方法小结(2种方法)
2016/07/18 PHP
简单JS代码压缩器
2006/10/12 Javascript
js之WEB开发调试利器:Firebug 下载
2007/01/13 Javascript
JS event使用方法详解
2008/04/28 Javascript
JavaScript对象模型-执行模型
2008/04/28 Javascript
一样的table?不一样的table(可编辑状态table)
2012/09/19 Javascript
window.navigate 与 window.location.href 的使用区别介绍
2013/09/21 Javascript
字段太多jquey快速清空表单内容方法
2014/08/21 Javascript
javascript判断图片是否加载完成的方法推荐
2016/05/13 Javascript
浅析vue数据绑定
2017/01/17 Javascript
JavaScript运动框架 多值运动(四)
2017/05/18 Javascript
理解 javascript 中的函数表达式与函数声明
2017/07/07 Javascript
React Native预设占位placeholder的使用
2017/09/28 Javascript
vue todo-list组件发布到npm上的方法
2018/04/04 Javascript
关于vue-router的那些事儿
2018/05/23 Javascript
Ajax获取node服务器数据的完整步骤
2020/09/20 Javascript
wxpython学习笔记(推荐查看)
2014/06/09 Python
Python实现多级目录压缩与解压文件的方法
2018/09/01 Python
pycharm远程开发项目的实现步骤
2019/01/20 Python
djang常用查询SQL语句的使用代码
2019/02/15 Python
python实现电子书翻页小程序
2019/07/23 Python
给你一面国旗 教你用python画中国国旗
2019/09/24 Python
导入tensorflow:ImportError: libcublas.so.9.0 报错
2020/01/06 Python
毕业生求职简历的自我评价
2013/10/23 职场文书
幼儿园家长评语
2014/02/10 职场文书
党员群众路线对照检查材料
2014/08/31 职场文书
会计电算化实训报告
2014/11/04 职场文书
外贸采购员岗位职责
2015/04/03 职场文书
2015年七一建党节活动方案
2015/05/05 职场文书
MySQL之DML语言
2021/04/05 MySQL
MySQL 可扩展设计的基本原则
2021/05/14 MySQL
详解python网络进程
2021/06/15 Python
python中__slots__节约内存的具体做法
2021/07/04 Python
MySQL 自动填充 create_time 和 update_time
2022/05/20 MySQL