1. 程式人生 > 程式設計 >開發Node CLI構建微信小程式腳手架的示例

開發Node CLI構建微信小程式腳手架的示例

本文介紹了Node CLI 構建微信小程式腳手架的示例,分享給大家,具體如下:

開發Node CLI構建微信小程式腳手架的示例

開發Node CLI構建微信小程式腳手架的示例

目的

由於目前公司的 TOC 產品只要是微信小程式,而且隨著業務的擴充套件,會有更多的需求,建立更多的小程式,為了讓團隊避免每次開發前花費大量時間做比如工程化的一些配置,以及保持每個專案的一致性,所以決定做一個 Node CLI 來建立微信小程式腳手架

  • 節省開發前期的大量時間,新專案可以很快開始業務開發
  • 保證專案統一性,有利於團隊間的協作及工程化
  • 提升團隊基建意識,從枯燥無味的業務開發中脫離出來,嘗試新的東西,即使很基礎很簡單

小程式選型

小程式的第三方框架有很多,我接觸過的就有 taro / wepy

/ mpvue ,並且都有對應上線的專案。 在嘗試這些框架的過程中,對比原生小程式,有一些感想想分享出來:

  • 第三方框架語法貼近vue/react,開發者可以根據自己的特點選擇框架,學習成本相對較低
  • 原生框架在CSS預處理,多端複用,狀態管理,自動構建這幾塊能力對比其他框架是欠缺的
  • 第三方框架額外的工具包會使打包體積變大,每次構建花費時間,同時效能不如原生
  • 第三方框架更新迭代很快,比如[email protected]/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,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,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構建小程式腳手架內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!