本文介绍了 Node CLI 构建微信小程序脚手架的示例,分享给大家,具体如下:
目的
由于目前公司的 TOC
产品只要是微信小程序,而且随着业务的扩展, 会有更多的需求,创建更多的小程序,为了让团队避免每次开发前花费大量时间做比如工程化的一些配置,以及保持每个项目的一致性, 所以决定做一个 Node CLI
来创建微信小程序脚手架
小程序选型
小程序的第三方框架有很多, 我接触过的就有 taro
/ wepy
/ mpvue
,并且都有对应上线的项目。 在尝试这些框架的过程中,对比原生小程序,有一些感想想分享出来:
综上所述,由于我们目前没有多端复用的要求,并且有的小程序相对简单,需要很短时间内开发完成, 最重要的是,其他的框架我都试过了,原生的还没写过,一个字,新鲜感!!: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
的配置,所以相对简单很多。
对于这个脚手架,相比于开发者工具创建的默认项目,我弥补了它的一些问题
- 默认项目太过简单,只适合自己折腾,对于团队或者企业,缺乏相应的代码约定/规范,没有强制的约定会导致团队协作间的困难,提升code review的难度,所以我在原来的基础上加入了eslint,stylelint,prettier,commitlint等配置,以及git hook 在 pre-commit 时,执行校验,确保提交的代码尽量规范 由于对 css 预处理的钟爱,另外加入了对 less 的支持,并且解决小程序背景图不支持本地图片的问题 由于以上基本都是文件处理,所以选择 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 仓库源码
参考