开发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 相关文章推荐
JavaScript 5 新增 Array 方法实现介绍
Feb 06 Javascript
jquery 实现上下滚动效果示例代码
Aug 09 Javascript
JavaScript实现的简单幂函数实例
Apr 17 Javascript
jquery实现滑屏大图定时收缩为小banner图片的广告代码
Sep 02 Javascript
JS函数arguments数组获得实际传参数个数的实现方法
May 28 Javascript
jQuery stop()用法实例详解
Jul 28 Javascript
搭建Bootstrap离线文档的方法
Dec 02 Javascript
整理关于Bootstrap过渡动画的慕课笔记
Mar 29 Javascript
微信小程序“摇一摇”的实例代码
Jul 20 Javascript
vue中英文切换实例代码
Jan 21 Javascript
js实现无缝轮播图特效
May 09 Javascript
JS中forEach()、map()、every()、some()和filter()的用法
May 11 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设计模式之结构模式的深入解析
2013/06/13 PHP
PHP使用ob_start生成html页面的方法
2014/11/07 PHP
php实现用于计算执行时间的类实例
2015/04/18 PHP
php lcg_value与mt_rand生成0~1随机小数的效果对比分析
2017/04/05 PHP
详解php反序列化
2020/06/10 PHP
javascript之typeof、instanceof操作符使用探讨
2013/05/19 Javascript
js实现右下角可关闭最小化div(可用于展示推荐内容)
2013/06/24 Javascript
JS鼠标滑过图片时切换图片实现思路
2013/09/12 Javascript
纯js和css完成贪吃蛇小游戏demo
2016/09/01 Javascript
Node.js + Redis Sorted Set实现任务队列
2016/09/19 Javascript
nodejs使用express创建一个简单web应用
2017/03/31 NodeJs
javascript实现二叉树的代码
2017/06/08 Javascript
让nodeJS支持ES6的词法----babel的安装和使用方法
2017/07/31 NodeJs
angularJS开发注意事项
2018/05/26 Javascript
JavaScript中filter的用法实例分析
2019/02/27 Javascript
JavaScript实现的九种排序算法
2019/03/04 Javascript
详解element-ui中form验证杂记
2019/03/04 Javascript
layui之table checkbox初始化时选中对应选项的方法
2019/09/02 Javascript
在vue+element ui框架里实现lodash的debounce防抖
2019/11/13 Javascript
iview实现动态表单和自定义验证时间段重叠
2021/01/10 Javascript
[00:20]TI9不朽观赛名额抽取
2019/08/05 DOTA
Python中列表、字典、元组、集合数据结构整理
2014/11/20 Python
Django Highcharts制作图表
2016/08/27 Python
python多维数组切片方法
2018/04/13 Python
详解Python基础random模块随机数的生成
2019/03/23 Python
Melijoe英国官网:法国儿童时尚网站
2016/11/18 全球购物
trivago美国:全球最大的酒店价格比较网站
2018/01/18 全球购物
自荐信要包含哪些内容
2013/11/06 职场文书
运动会领导邀请函
2014/01/10 职场文书
《飞向蓝天的恐龙》教学反思
2014/04/09 职场文书
求职信怎么写
2014/05/23 职场文书
毕业证代领委托书
2014/09/26 职场文书
2015年妇委会工作总结
2015/05/22 职场文书
中考百日冲刺决心书
2015/09/22 职场文书
十大最强岩石系宝可梦,怪颚龙实力最强,第七破坏力很强
2022/03/18 日漫
Lakehouse数据湖并发控制陷阱分析
2022/03/31 Oracle