開發(fā)Node CLI構(gòu)建微信小程序腳手架的示例
本文介紹了 Node CLI 構(gòu)建微信小程序腳手架的示例,分享給大家,具體如下:
目的
由于目前公司的 TOC 產(chǎn)品只要是微信小程序,而且隨著業(yè)務(wù)的擴(kuò)展, 會(huì)有更多的需求,創(chuàng)建更多的小程序,為了讓團(tuán)隊(duì)避免每次開發(fā)前花費(fèi)大量時(shí)間做比如工程化的一些配置,以及保持每個(gè)項(xiàng)目的一致性, 所以決定做一個(gè) Node CLI 來創(chuàng)建微信小程序腳手架
- 節(jié)省開發(fā)前期的大量時(shí)間,新項(xiàng)目可以很快開始業(yè)務(wù)開發(fā)
- 保證項(xiàng)目統(tǒng)一性,有利于團(tuán)隊(duì)間的協(xié)作及工程化
- 提升團(tuán)隊(duì)基建意識(shí),從枯燥無味的業(yè)務(wù)開發(fā)中脫離出來,嘗試新的東西,即使很基礎(chǔ)很簡單
小程序選型
小程序的第三方框架有很多, 我接觸過的就有 taro / wepy / mpvue ,并且都有對應(yīng)上線的項(xiàng)目。 在嘗試這些框架的過程中,對比原生小程序,有一些感想想分享出來:
- 第三方框架語法貼近vue/react, 開發(fā)者可以根據(jù)自己的特點(diǎn)選擇框架,學(xué)習(xí)成本相對較低
- 原生框架在CSS預(yù)處理,多端復(fù)用,狀態(tài)管理,自動(dòng)構(gòu)建這幾塊能力對比其他框架是欠缺的
- 第三方框架額外的工具包會(huì)使打包體積變大,每次構(gòu)建花費(fèi)時(shí)間,同時(shí)性能不如原生
- 第三方框架更新迭代很快,比如wepy@1.x/wepy@2, 導(dǎo)致舊項(xiàng)目的更新問題
- 小程序的特性更新迭代速度較快, 第三方框架會(huì)相對滯后
綜上所述,由于我們目前沒有多端復(fù)用的要求,并且有的小程序相對簡單,需要很短時(shí)間內(nèi)開發(fā)完成, 最重要的是,其他的框架我都試過了,原生的還沒寫過,一個(gè)字,新鮮感?。?smile: ,所以最終當(dāng)仁不讓地選擇了原生小程序,不得不說,原生大法就是妙?。?:clap::clap::clap::clap:
大體思路
這個(gè)功能是相對很基礎(chǔ)的,但是作為一個(gè)每天搬磚的業(yè)務(wù)仔來說,是個(gè)艱難的過程,也是個(gè)很好的學(xué)習(xí)機(jī)會(huì)。
在做之前,想找找個(gè)社區(qū)比較:ox::beer:的學(xué)習(xí)(抄)一下,短暫考慮后,果斷選擇 taro-cli , 然后火速打開源碼,一頓操作(完全蒙圈),學(xué)習(xí)了一點(diǎn)之后,才開始上手
這個(gè)具體的實(shí)現(xiàn)思路我想到兩個(gè)
git clone遠(yuǎn)程倉庫作為模版下載到本地,再根據(jù)用戶輸入配置修改.json文件(比如appId)template就放在當(dāng)前目錄中,直接`copy``, 之后的事等同
權(quán)衡之后,打算使用 lerna 作為管理工具, 其中模版也作為一個(gè) npm 包 ,用到的時(shí)候去 npm 下載,這么做我是為了方便管理,統(tǒng)一 push / publish , 就是為了省事 :smile:。
最終思路:
暴露命令 —> 用戶交互輸入配置 -> 集合配置下載模版 -> 根據(jù)配置修改 .json -> git init + 安裝依賴
開發(fā) Node CLI
Lerna 項(xiàng)目搭建
知道 monorepo 的同學(xué)不需要我多說,其實(shí)就是把代碼放在一個(gè)倉庫里,結(jié)果包之間回想以來,發(fā)布繁瑣等問題, 這里我們就用到了 lerna 這個(gè)神器幫助我們做包的統(tǒng)一管理
// 創(chuàng)建項(xiàng)目 mkdir modoo-mini-program cd modoo-mini-program // 初始化 lerna init cd packages mkdir modoo-script mkdir modoo-template-mini mkdir modoo-mini // 安裝 modoo-script 依賴用于測試,無其他實(shí)際用處 lerna bootstrap // 安裝依賴 + npm link
安裝依賴
為了實(shí)現(xiàn)功能,我們需要安裝一些依賴包
- commander 命令行工具,用于讀取命令參數(shù),作對應(yīng)操作
- node-fs-extra 在 Node.js 的 fs 基礎(chǔ)上增加了一些新的方法,更好用,還可以拷貝模板。
- chalk 可以用于控制終端輸出字符串的樣式, 調(diào)整顏色啥的
- inquirer 用戶命令行交互,獲取用戶的交互配置數(shù)據(jù),就像個(gè)提問板
- ora 實(shí)現(xiàn)加載中的狀態(tài)是一個(gè) Loading 加前面轉(zhuǎn)起來的小圈圈,成功了是一個(gè) Success 加前面一個(gè)小鉤鉤。
- log-symbols 日志彩色符號(hào),用來顯示√ 或 × 等的圖標(biāo)
獲取命令
首先第一步,要在用戶全局安裝之后,暴露出命令接口,需要在 packages.json 文件中加入如下內(nèi)容
"bin": {
"modoo-script": "./bin/modoo-script.js"
},
之后在根目錄下創(chuàng)建 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 命令,床架項(xiàng)目
.command("init [projectName]", "Init a project with default templete")
.parse(process.argv); // 解析命令參數(shù)
然后需要注意的是, commander 支持 Git 風(fēng)格的子命令處理,可以根據(jù)子命令自動(dòng)引導(dǎo)到以特定格式命名的命令執(zhí)行文件,文件名的格式是 [command]-[subcommand] ,例如:
modoo-script init => modoo-script-init modoo-script build => modoo-script-build
所以為了實(shí)現(xiàn) init 命令,可以直接在 bin 文件目錄下添加 modoo-script-init.js
#!/usr/bin/env node
const { program } = require("commander");
program
.option("--name [name]", "項(xiàng)目名稱")
.option("--description [description]", "項(xiàng)目介紹")
.option("--framework", "腳手架框架")
.parse(process.argv);
const args = program.args;
// 獲取命令參數(shù)
const { name, description, framework } = program;
const projectName = args[0] || name;
......
用戶交互
獲取了命令參數(shù)后,根據(jù)參數(shù)轉(zhuǎn)到用戶交互界面,這里使用的是 inquirer 來處理命令行交互, 用法很簡單
const inquirer = require('inquirer')
if (typeof conf.description !== 'string') {
prompts.push({
type: 'input',
name: 'description',
message: '請輸入項(xiàng)目介紹!'
})
}
......
inquirer.prompt(prompts).then(answers => {
// 整合配置
this.conf = Object.assign(this.conf, answers);
})
遠(yuǎn)程模塊
這里較為折騰,一開始說了,我把模版作為 npm包 ,具體查找,下載的過程如下
npm search查找相應(yīng)的模版npm 包- 在用戶選擇框架后對應(yīng)所需的包,獲取它的詳細(xì)信息,主要是
tarball - 用戶輸入完后,下載
tarball到項(xiàng)目目錄,并修改.json文件配置
部分代碼如圖所示
// 一 npm search 查找相應(yīng)的模版 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 數(shù)據(jù)
const pkg = require("package-json");
const chalk = require("chalk");
const logSymbols = require("log-symbols");
exports.getBoilerplateMeta = framework => {
log(
logSymbols.info,
chalk.cyan(`您已選擇 ${framework} 遠(yuǎn)程模版, 正在查詢該模版...`)
);
return pkg(framework, {
fullMetadata: true
}).then(metadata => {
const {
dist: { tarball },
version,
name,
keywords
} = metadata;
log(
logSymbols.success,
chalk.green(`已為您找到 ${framework} 遠(yuǎn)程模版, 請輸入配置信息`)
);
return {
tarball,
version,
keywords,
name
};
});
};
// 三 下載 npm 包
const got = require("got");
const tar = require("tar");
const ora = require("ora");
const spinner = ora(
chalk.cyan(`正在下載 ${framework} 遠(yuǎn)程模板倉庫...`)
).start();
const stream = await got.stream(tarball);
fs.mkdirSync(proPath);
const tarOpts = {
strip: 1,
C: proPath
};
// 管道流傳輸下載文件到當(dāng)前目錄
stream.pipe(tar.x(tarOpts)).on("close", () => {
spinner.succeed(chalk.green("下載遠(yuǎn)程模塊完成!"));
......
})
// 四 遍歷文件修改配置
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()];
});
};
下載依賴
下載完畢并且修改完配置后, 默認(rèn)執(zhí)行 git init + 根據(jù)環(huán)境( yarn / npm / cnpm )安裝依賴,這個(gè)就很簡單了
const { exec } = require("child_process");
const ora = require("ora");
const chalk = require("chalk");
// proPath 項(xiàng)目目錄
process.chdir(proPath);
// git init
const gitInitSpinner = ora(
`cd ${chalk.cyan.bold(projectName)}, 執(zhí)行 ${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(
`執(zhí)行安裝項(xiàng)目依賴 ${chalk.cyan.bold(command)}, 需要一會(huì)兒...`
).start();
exec(command, (error, stdout, stderr) => {
if (error) {
installSpinner.color = "red";
installSpinner.fail(chalk.red("安裝項(xiàng)目依賴失敗,請自行重新安裝!"));
console.log(error);
} else {
installSpinner.color = "green";
installSpinner.succeed("安裝成功");
log(`${stderr}${stdout}`);
}
});
主要的代碼就是這些,其實(shí)只要知道思路,這些東西都很簡單,雖然我寫的有點(diǎn) ️:chicken:,但是主要的邏輯還是能理清楚的一些的。更加詳細(xì)的可以去:eyes:我發(fā)的源碼,多謝指教。:pray::pray::pray:
開發(fā)腳手架
因?yàn)檫@是小程序的腳手架,它不像其他 web 框架一樣需要很多 webpack 的配置,所以相對簡單很多。
對于這個(gè)腳手架,相比于開發(fā)者工具創(chuàng)建的默認(rèn)項(xiàng)目,我彌補(bǔ)了它的一些問題
- 默認(rèn)項(xiàng)目太過簡單,只適合自己折騰,對于團(tuán)隊(duì)或者企業(yè),缺乏相應(yīng)的代碼約定/規(guī)范,沒有強(qiáng)制的約定會(huì)導(dǎo)致團(tuán)隊(duì)協(xié)作間的困難,提升code review的難度,所以我在原來的基礎(chǔ)上加入了eslint,stylelint,prettier,commitlint等配置,以及git hook 在 pre-commit 時(shí),執(zhí)行校驗(yàn),確保提交的代碼盡量規(guī)范
- 由于對 css 預(yù)處理的鐘愛,另外加入了對 less 的支持,并且解決小程序背景圖不支持本地圖片的問題
- 由于以上基本都是文件處理,所以選擇 gulp 作為構(gòu)建工具,這里是 v4, 與v3 寫法上有一定的區(qū)別,不過關(guān)系不大
在根目錄下創(chuàng)建 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; // 原目錄
})
);
});
// 開發(fā)環(huán)境監(jiān)聽 less
if (env === 'development') {
gulp.watch(['./miniprogram/**/*.less'], gulp.series('less')).on('change', (path) => {
log(chalk.greenBright(`File ${path} was changed`));
});
}
// 一下代碼注釋掉了,依賴包下載太慢了,這主要負(fù)責(zé)圖片的壓縮
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 倉庫源碼
參考
到此這篇關(guān)于開發(fā)Node CLI構(gòu)建微信小程序腳手架的示例的文章就介紹到這了,更多相關(guān)Node CLI構(gòu)建小程序腳手架內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nodejs項(xiàng)目windows下開機(jī)自啟動(dòng)的方法
今天小編就為大家分享一篇nodejs項(xiàng)目windows下開機(jī)自啟動(dòng)的方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-11-11
Nodejs如何進(jìn)行性能監(jiān)控和分析優(yōu)化
Node.js應(yīng)用可能因?yàn)楦卟l(fā)、內(nèi)存泄漏、CPU密集型任務(wù)等原因?qū)е滦阅芟陆?影響用戶體驗(yàn)甚至系統(tǒng)穩(wěn)定性,通過性能監(jiān)控和分析,我們可以及時(shí)發(fā)現(xiàn)潛在問題,并針對性地進(jìn)行優(yōu)化,確保系統(tǒng)正常運(yùn)行且具備良好的性能表現(xiàn)2024-06-06
關(guān)于node.js版本npm -v報(bào)錯(cuò)問題的解決方法
最近工作中遇到了些問題,這里總結(jié)下,下面這篇文章主要給大家介紹了關(guān)于node.js版本npm -v報(bào)錯(cuò)問題的解決方法,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
如何構(gòu)建一個(gè)?NodeJS?影院微服務(wù)并使用?Docker?部署
微服務(wù)是一個(gè)單獨(dú)的自包含單元,與其他許多單元一起構(gòu)成一個(gè)大型應(yīng)用程序,這篇文章主要介紹了如何構(gòu)建一個(gè)NodeJS影院微服務(wù)并使用Docker部署,在這個(gè)系列中,將構(gòu)建一個(gè) NodeJS 微服務(wù),并使用 Docker Swarm 集群進(jìn)行部署,需要的朋友可以參考下2023-08-08
node實(shí)現(xiàn)簡單的反向代理服務(wù)器
本篇文章主要介紹了node實(shí)現(xiàn)簡單的反向代理,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07

