003. 如何將老項目的小程序快速改成分包模式

背景

在最開始準備製做小程序的時候,爲了求開發的快速,所使用了直接page的引入方式,每次小程序加載都是全包下載。3月份以前小程序頁面穩定在35個,下載耗時在1800ms左右,4月份初上線了新業務頁面膨脹到52個頁面時,下載耗時基本穩定在2300ms左右,隨着用戶反饋愈來愈多,因此開始準備小程序採用分包加載,來解決這個問題。5月份在分包加載以後,頁面依舊穩定在52個,可是下載耗時穩定800ms左右,縮短了將近1500ms。基本作到了0業務入侵、加入npm script 來實現新建頁面,同時兼容分包加載,page加載。javascript

操做流程

執行命令java

npm run new
複製代碼

給出信息提示,而且要求輸入路徑node

[Info] 幫助你快速建立page文件
[Info] 只侷限於新目錄建立
[Info] 請輸入文件路徑,已幫你省略pages/
[Info] 例子:address/addressList
? 請輸入文件路徑:
複製代碼

輸入路徑以後建立文件的提示shell

[Info] 已建立base64.js
[Info] 已建立index.js
[Info] 已建立index.json
[Info] 已建立index.wxml
[Info] 已建立index.wxss
[Info] 建立完成!!!!
複製代碼

選擇包,是主包、仍是分包,而且提示,給出提示,若是是tabBar中使用,只能選擇主包,而且手動配置app.json的tabBarnpm

? 你想生成什麼樣的包: (Use arrow keys)
❯ 主包,在pages中寫入,若是是tabBar中使用,只能選擇主包,而且手動配置app.json的tabBar
  分包,在subpackages中寫入
複製代碼

單選完成以後提示新建成功json

[Info] 寫入成功
複製代碼

第一次分包

首先咱們對未分包和分包的app.json代碼進行對比小程序

// 未分包
"pages": [
  "pages/a/index",
  "pages/b/index",
  "pages/b/list",
]
複製代碼
// 實現分包
"subpackages": [
  {
    "root": "pages/a",
    "pages": [
      "index",
    ]
  },
  {
    "root": "pages/b",
    "pages": [
      "index",
      "list"
    ]
  }
]
複製代碼

接下來看目錄結構數組

pages
  |---- a
        |---- index.js
  |---- b
        |---- index.js
        |---- list.js
app.json
複製代碼

簡單分一下,第一次分包的核心業務是,將pages這個普通列表,變成名爲subpackages的二叉樹結構,左子節點是root,右子節點是pages,而且右子節點是string類型,左子節點是array類型。好吧,我能夠定下規則,首先我能夠將pages/a/index這個字符串變爲數組以/爲分界的['pages','a','index']數組,而後我將前兩項做爲root節點的參數,用slice操做截取出來,轉爲字符串類型,賦值給root。以後的全部參數,我都賦值或者push給pages參數,最後我就獲得了這樣一個符合要求的樹,最後push進subpackages便可。至於數據源,我只須要讀出app.json這個文件的pages參數,而且經過個人方法寫入subpackages參數便可。bash

要點數據結構

  • 去重,由於subpackages內,root節點是惟一的,可是在pages裏面可能我會截取初重複的值

解決

  • 使用Map結構,把截取出的root值做爲key,剩下的做爲value,Map數據這種數據結構的特色就是key值惟一。
let list = [
  "pages/a/index",
  "pages/a/list",
  "pages/a/detail/index",
  "pages/c/list",
  "pages/b/index",
];

let m = new Map();
let packages = [];
list.forEach(v=>{
  let arr = v.split('/');
  let root = arr.splice(0,2).join('/');
  let pages = arr.join('/');

  if(m.get(root)){
    let s = m.get(root);
    m.set(root,[...s, pages]);
  }else{
    m.set(root,[pages]);
  };
});
for(let [key,value] of m){
  packages.push({
    root: key,
    pages: value,
  })
}

console.log(packages);
複製代碼
// log 出的結構
[
  { root: 'pages/a', pages: [ 'index', 'list', 'detail/index' ] },
  { root: 'pages/c', pages: [ 'list' ] },
  { root: 'pages/b', pages: [ 'index' ] }
]
複製代碼

我已經在不入侵業務的狀況下實現了小程序的第一次分包,節約了我手動去改的勞動力,我我的認爲,解決問題,上策用數據結構,中策寫兼容代碼,下策手動去改。至於,讀出寫入文件,我就不贅述了,google便可。

建立新pages的指令編寫

好吧,我實現了第一次的分包。而後我要思考,若是我每次要加頁面的話,是否是就要去查看subpackages,找到對應的root,而且添加pages。這麼重複勞動力的操做,我爲何不用腳本替代呢,是吧。

核心需求

  • 編寫交互式的命令輸入
  • 檢測輸入的page目錄是否存在,存在就報錯
  • 不存在建立page目錄,複製template到新建的page中
  • 根據使用者的單選,選擇寫入pages,或者寫入subpackages
  • 在page文件加中設置預留文件夾,用作初級業務拆分,不加入subpackages檢測
  • 屏蔽app.json中pages的文件,不加入subpackages檢測

設計npm script

{
  "subcontract": "node ./config/subcontract",
  "new": "node ./config/new"
}  
複製代碼

添加的package.json參數,ignore-files表示文件夾中不檢測的文件,pages表式app.json中的文件

{
  "ignore-files": [
    "**/common/**",
    "**/component/**",
    "**/<name>/**",
    "**/<name>/**",
    "**/<name>/**",
  ],
  "pages": [
    "pages/<name>/index",
    "pages/<name>/index",
    "pages/<name>/index",
  ]
}
複製代碼

需求已經明確,我就要去找我須要用到的npm包了

colors        命令行顏色
inquirer      交互式命令行
glob          全局搜索文件
fs-extra      文件寫入寫出
path          路徑
shelljs       執行shell命令
複製代碼

分析new.js文件

const colors = require('colors');
const inquirer = require('inquirer');
const glob = require('glob');
const fs = require('fs-extra');
const path = require('path');
const shell = require('shelljs');
const PKG = require('../package.json');
const ROOT = path.resolve(__dirname, '../');

let appJson = require('../app.json');
const promps = [{
  type: 'input',
  name: 'pagePath',
  message: '請輸入文件路徑:',
},
{
  type: 'list',
  name: 'type',
  message: '你想生成什麼樣的包:',
  choices: [
    {
      name: '主包,在pages中寫入,若是是tabBar中使用,只能選擇主包,而且手動配置app.json的tabBar',
      value: '1',
    },
    {
      name: '分包,在subpackages中寫入',
      value: '2',
    },
  ],
}];
const logger = {
  info(msg) {
    console.log(`[Info] ${colors.green(msg)}`);
  },
  warn(msg) {
    console.log(`[Warn] ${colors.yellow(msg)}`);
  },
  error(msg) {
    console.log(`[Error] ${colors.red(msg)}(/‵Д′)/~ ╧╧`);
  },
};

logger.info('幫助你快速建立page文件');
logger.info('只侷限於新目錄建立');
logger.info('請輸入文件路徑,已幫你省略pages/');
logger.info('例子:xxxxx/xxxx');
複製代碼

這是代碼中的常量部分和默認提示部分,我寫了logeer對象來做爲提示輸出的默認顏色,promps做爲我交互命令行的基礎配置。引入package.json個人主要目的是由於我屏蔽了一些文件ignore-filespages,想這兩個參數的文件夾我是不會被檢測的,不會被加入到subpackages的。

function checkFile(name) {
  const options = {
    ignore: [
      '**/*.js',
      '**/*.wxss',
      '**/*.wxml',
      '**/*.json',
    ],
    cwd: 'pages/',
  };
  const files = glob.sync('**', options);
  if (files.some((v) => v === name)) {
    logger.error('輸入的目錄已經存在,已終止!!!!');
    return false;
  };
  return name;
};
複製代碼

在這是檢測文件是否存在的方法,只須要傳入路徑,便可檢測這個路徑是否在pages/目錄中存在。

function buildFile(name) {
  const options = {
    cwd: 'template/page',
  };
  const files = glob.sync(`**`, options);

  files.forEach((v)=>{
    const file = v.split('.tp')[0];
    fs.copy(`${ROOT}/template/page/${v}`, `${ROOT}/pages/${name}/${file}`, (err) => {
      if (err) {
        console.error(err);
        return false;
      }
    });
    logger.info(`已建立${file}`);
  });
  logger.info('建立完成!!!!');
  return true;
};
複製代碼

這是複製文件夾而且複製模版文件的方法,我準備了tempalte這個文件夾,用來存儲我寫的模版文件,建立完成以後,我直接複製進去便可。爲了和普通文件區別,我添加的.tp後綴。個人模版是可擴展的,我能夠把requestapp({...})在其中寫好,而且添加我我的的一些方法。

function subcontract(res) {
  inquirer.prompt(promps[1]).then((answers)=>{
    if (answers.type === '1') {
      PKG['ignore-files'].push(`${res}/**`);
      PKG['pages'].push(`${res}/index`);
      appJson['pages'].push(`pages/${res}/index`);
      fs.writeFileSync(`${ROOT}/app.json`, JSON.stringify(appJson, null, 2));
      fs.writeFileSync(`${ROOT}/package.json`, JSON.stringify(PKG, null, 2));
      logger.info('寫入成功');
    };
    if (answers.type === '2') shell.exec('npm run subcontract');
  });
};
複製代碼

這是選擇pages仍是subcontract的方法,選擇了subcontract,我直接執行我上面寫的小程序分包方法subcontract.js便可。若是選擇pages,我會將它加入package.json中的pages對象,這個對象表式這些文件名不被subcontract腳本檢測。

async function inquirers() {
  const {pagePath} = await inquirer.prompt(promps[0]);
  const path = pagePath.replace(/\s+/g, '');

  if (!path) {
    logger.error('輸入有失誤,已終止!!!!');
    return false;
  };
  if (/.*[\u4e00-\u9fa5]+.*$/.test(path)) {
    logger.error('請不要輸入中文符號,已終止!!!!');
    return false;
  };

  return path;
};
複製代碼

檢測輸入值是否合法,是否有中文,而且去除空格

( async function() {
  const inquirerRes = await inquirers();
  const checkFileRes = inquirerRes && checkFile(inquirerRes);
  const buildFileRes = checkFileRes && buildFile(checkFileRes);
  buildFileRes && subcontract(checkFileRes);
})();
複製代碼

最後組裝,inquirerRes變量負責判斷輸入值是否正確。而後進入checkFile,來檢測文件夾是否重複。調用buildFile方法,建立文件夾,複製模版文件。最後調用subcontract來判斷是分包仍是主包。

subcontract.js 分析

const glob = require('glob');
const fs = require('fs');
const path = require('path');
const colors = require('colors');
const ROOT = path.resolve(__dirname, '../');
const PAG = require('../package.json');
let appJson = require('../app.json');

const ignoreFiles = PAG['ignore-files'];
const pages = PAG['pages'];
const logger = {
  info(msg) {
    console.log(`[Info] ${colors.green(msg)}`);
  },
  warn(msg) {
    console.log(`[Warn] ${colors.yellow(msg)}`);
  },
  error(msg) {
    console.log(`[Error] ${colors.red(msg)}(/‵Д′)/~ ╧╧`);
  },
};

const subcontract = () => {
  const options = {
    ignore: ignoreFiles,
    cwd: 'pages/',
  };
  const files = glob.sync('**/index.js', options);

  let subcontractMap = new Map();
  files.forEach((v)=>{
    let arr = v.split('.')[0].split('/');
    let root = arr.shift();
    let page = arr.join('/');

    if (subcontractMap.has(root)) {
      let pages = subcontractMap.get(root);
      pages.push(page);
      subcontractMap.set(root, pages);
    } else {
      subcontractMap.set(root, [page]);
    }
  });

  let subcontractList = [];
  subcontractMap.forEach((v, k)=>{
    subcontractList.push({
      root: `pages/${k}`,
      pages: v,
    });
  });

  return subcontractList;
};

appJson.subpackages = subcontract();
appJson.pages = pages;
fs.writeFileSync(`${ROOT}/app.json`, JSON.stringify(appJson, null, 2));
logger.info('寫入成功');
複製代碼

這個方法其實和小程序第一次分包的方法大同小異。只不過我修改了數據源的獲取,第一次我是讀取app.json的pages,這裏我是根據目錄來的,以及加入了ignoreFiles來作文件屏蔽。以及一些友好提示。

結尾

項目優化的道路還有很長這只是最最初步的方案。爲何我開始不直接選擇分包呢?由於項目開始的時候尚未分包,並且若是有,我感受分包機制的這種書寫方式,可能會帶給開發者出錯的可能性,我爲了將項目工期縮短,出錯可能性下降,我也不會選擇一開始就分包。當業務增加到必定量,以及業務逐漸趨向於穩定時候,我就能夠根據業務的特性,去作相對應的事情。這種方式我稱之爲技術迭代。在特定的時候,選擇特定的解決方案,堅定不過分設計。

相關文章
相關標籤/搜索