小程序gulp構建優化之道

1. 背景

衆所周知,小程序主包的大小限制在 2M 之內,對於日益龐大的工程項目,開發者們無所不用其極地進行優化。原生小程序基本採用gulp進行構建,可否在gulp構建流程中作文章,達到「優化」的目的。css

首先明確gulp構建優化的目的是壓縮小程序包體積,提高用戶開發體驗。做者在熟悉gulp開發後,推翻了原有的構建流程,從新設計,本着精益求精的目的進行優化,取得以下三點進展:vue

  1. 小程序包體積減少:優化前包體積 2212KB,主包體積 1668.9KB;優化後包體積 2012KB,主包體積 1470.7KB;包體積減少 9.04%,主包體積減少 11.9%
  2. 構建時間縮短:優化前 dev 模式構建時間 58 秒,build 模式構建時間 62 秒;優化後 dev 模式構建時間 27 秒,build 模式構建時間 32 秒;dev 模式構建時間縮短 53.4%,build 模式構建時間縮短 48.4%
  3. 用戶開發體驗提高:優化前須要點擊開發者工具中的菜單欄:工具 --> 構建 npm,生成小程序專用的npm包才能成功編譯代碼,同時經常會由於各類路徑引入問題致使不得不從新編輯或者從新構建 npm。優化後再無此類煩惱,gulp 構建時自動分析生成小程序專用的npm包,大大提升了開發效率。

2. gulp 工做流架構

gulp構建優化

對小程序來講,除了app.js做爲程序入口以外,每一個 page 頁面均可以做爲一個頁面入口,更傾向是固定路徑模式的多頁應用。gulp 構建的目的是將開發路徑的代碼翻譯轉到小程序專用路徑,該路徑下的代碼可以被微信開發者工具讀取、編譯、構建。經過 gulp 工具可實現:node

  • .ts 文件編譯爲 .js.less 文件編譯爲 .wxss ,以支持 TypeScriptLess 語法。
  • 支持 sourcemaps 方便錯誤調試與定位。
  • 壓縮圖片和各種文件,減小小程序代碼包大小。
  • 分析代碼,依賴自動提取,支持提取普通 npm 包與小程序專用 npm 包。
  • 其他文件將直接拷貝至目標路徑。
  • 添加watch,方便開發者調試。

2.1 拆分 task

採用原生框架開發的小程序主要有.js.json.wxml.wxss四種文件構成,爲了提高開發效率,一般會引入.ts.less文件。因爲每種文件的編譯構建方法不盡相同,所以須要爲不一樣類型的文件建立不一樣的taskwebpack

const src = './src';

// 文件匹配路徑
const globs = {
  ts: [`${src}/**/*.ts`, './typings/index.d.ts'], // 匹配 ts 文件
  js: `${src}/**/*.js`, // 匹配 js 文件
  json: `${src}/**/*.json`, // 匹配 json 文件
  less: `${src}/**/*.less`, // 匹配 less 文件
  wxss: `${src}/**/*.wxss`, // 匹配 wxss 文件
  image: `${src}/**/*.{png,jpg,jpeg,gif,svg}`, // 匹配 image 文件
  wxml: `${src}/**/*.wxml`, // 匹配 wxml 文件
  other:[`${src}/**`,`!${globs.ts[0]}`,...] // 除上述文件外的其它文件
};

// 建立不一樣的task
const ts = cb => {}; // 編譯ts文件
const js = cb => {}; // 編譯js文件
...
const copy = cb => {}; // 除上述文件外的其它文件複製到目標文件夾
複製代碼

2.2 dev 和 build 模式區分

如約定俗成通常,咱們一般在dev模式下進行開發調試,在build模式下進行發佈,這兩種的 gulp 構建方案是須要區分的。如代碼所示:dev模式下須要添加watch來監聽文件的變化,及時地從新進行構建,同時須要添加sourcemap便於調試;而build模式下更須要的是對文件進行壓縮以減小包體積。git

// 默認dev模式配置
let config = {
  sourcemap: true, // 是否開啓sourcemap
  compress: false, // 是否壓縮wxml、json、less等各類文件
  ...
};
// 修改爲build模式配置
const setBuildConfig = cb => {
  config = {...}; cb();
};
// 併發執行全部文件構建task
const _build = gulp.parallel(copy, ts, js, json, less, wxss, image, wxml);
// build模式構建
const build = gulp.series(
  setBuildConfig, // 設置成build模式配置
  gulp.parallel(clear, clearCache), // 清除目標目錄文件和緩存
  _build, // 併發執行全部文件構建task
  ...
);
// dev模式構建
const build = gulp.series(
  clear, // 清除目標目錄文件
  _build, // 併發執行全部文件構建task
  ...
  watch, // 添加監聽
);
複製代碼

3. 優化之道

前面的篇幅講述了 gulpfile 文件總體架構設計,下面講述每一個task的具體配置,以及優化之道。github

3.1 npm 構建優化

3.1.1 官方通用方案的不足之處

① 安裝 npm 包web

安裝 npm 包的方法有如下兩種:typescript

  1. 手動檔: 在小程序 package.json 所在的目錄中執行命令npm install安裝 npm 包,此處要求參與構建 npm 的 package.json 須要在 project.config.js 定義的 miniprogramRoot 以內。
  2. 自動檔: 經過gulptask進行處理,建立一個task,將根目錄的package.json文件拷貝到小程序所在目錄(本文命名爲miniprogram),經過exec執行cd miniprogram && npm install --no-package-lock --production命令安裝 npm 包。代碼以下所示:
gulp.task('module:install', (cb) => {
	const destPath = './miniprogram/package.json';
	const sourcePath = './package.json';
	try {
		// ...省略代碼,判斷是否有 package.json 的變更,無變更則返回
		// 複製文件
		fs.copyFileSync(path.resolve(sourcePath), path.resolve(destPath));
		// 執行命令
		exec(
			'cd miniprogram && npm install --no-package-lock --production',
			(err) => {
				if (err) process.exit(1);
				cb();
			}
		);
	} catch (error) {
		// ...
	}
});
複製代碼

② 構建 npm 包npm

衆所周知,咱們使用npm install構建的 npm 包都會在node_modules目錄,可是小程序規定node_modules目錄不會參與編譯、上傳和打包中,因此小程序想要使用 npm 包必須走一遍 「構建 npm」 的過程,即點擊開發者工具中的菜單欄:工具 --> 構建 npm。這時,node_modules 的同級目錄下會生成一個 miniprogram_npm 目錄,裏面會存放構建打包後的 npm 包,也就是小程序真正使用的 npm 包。 構建npm先後目錄結構變化json

如上圖所示,小程序真正使用的 npm 包和node_modules目錄下的 npm 包的結構是有差別的,這個差別就是點擊構建 npm這個操做執行的打包過程,分爲兩種:小程序 npm 包會直接拷貝構建文件生成目錄下的全部文件到 miniprogram_npm 中;其餘 npm 包則會從入口 js 文件開始走一遍依賴分析和打包過程(相似 webpack)。

顯而易見,官方提供的 npm 構建方案暴露了如下兩點問題:

  1. 耗時長:經過gulp打包構建小程序時,第一個task就是在miniprogram目錄安裝 npm 包,當依賴的 npm 包數量夠多時,會消耗大量的時間。
  2. 開發流程繁瑣:經過小程序開發者工具調試代碼時,當引入新的 npm 包或者原有的 npm 包更新時,都要從新點擊菜單欄:工具 --> 構建 npm,對開發者十分不友好。

所以咱們但願能借助gulp工做流,在小程序構建時分析每一個文件的依賴關係,將須要的 npm 包拷貝至 miniprogram_npm目錄。一步到位,直接省略了上述安裝npm包構建npm包兩個步驟。

3.1.2 gulp-mp-npm 實現依賴分析與提取

所幸,社區無所不能,有做者開發了一個用以小程序提取 npm 依賴包的 gulp 插件 gulp-mp-npm ,有如下特色:

  • 依賴分析,僅會提取使用到的依賴與組件。
  • 支持提取普通 npm 包與小程序專用 npm 包。
  • 不會對依賴進行編譯與打包(交給微信開發者工具或者其餘 gulp 插件完成)。
  • 兼容官方方案及原理,同時支持自定義 npm 輸出文件夾。

前面提到過構建npm包時,針對小程序 npm 包會直接拷貝構建文件生成目錄下的全部文件到 miniprogram_npm 中;對於其它普通 npm 包則會從入口 js 文件開始走一遍依賴分析和打包過程(相似 webpack),打包生成的代碼在同級目錄下會生成 source map 文件,方便進行逆向調試。而gulp-mp-npm插件僅僅經過依賴分析,提取使用到的依賴和組件,將其複製到至 miniprogram_npm目錄下對應的 npm 包文件夾內,並不會對依賴進行編譯與打包,這一步交由微信開發者工具完成。

gulp-mp-npm構建原理

gulp-mp-npm的構建原理如上圖所示,想要深刻了解能夠參考: 小程序 npm 依賴包 gulp 插件設計

插件的使用能夠根據項目需求,在 gulpfile.js 中進行以下配置:

const gulp = require('gulp');
const mpNpm = require('gulp-mp-npm')

const js = () => gulp.src('src/**/*.js')
    .pipe(mpNpm()) // 分析提取 js 中用到的依賴
    .pipe(gulp.dest('dist'));

const less = () => gulp.src('src/**/*.less')
    .pipe(gulpLess()) // 編譯less
    .pipe(rename({ extname: '.wxss' }))
    .pipe(mpNpm()) // 分析提取 less 中用到的依賴
    .pipe(gulp.dest('dist'));
...
複製代碼

一般在.ts.js.json.less.wxss 等文件中都會有可能使用到npm 依賴。所以,插件 gulp-mp-npm 在上述 5 個 tasks 中都需執行。分析 .json 文件是由於插件會嘗試讀取小程序頁面配置中 usingComponents 字段,提取使用的 npm 小程序組件。

3.1.3 數據對比

npm構建優化數據對比

3.2 watch 增量編譯

衆所周知,gulptask都是單次執行的。某個文件變化後 gulp.watch 會從新執行整個 task 來完成構建,這樣會致使未變化的文件重複構建,效能較低。能夠採起如下兩個措施進行效率優化:

3.2.1 拆分task,合理建立watch

前面提到,根據文件類型不一樣,劃分紅copy, ts, js, json, less, wxss, image, wxml八種不一樣的task,分別爲每種task建立watch。假設修改index.ts文件,只會從新執行ts這個task,其它task不受影響。代碼以下所示:

const watchOptions = { events: ['add', 'change', `unlink`] };
const watch = () => {
  gulp.watch(globs.copy, watchOptions, copy);
  gulp.watch(globs.ts, watchOptions, ts);
  gulp.watch(globs.js, watchOptions, js);
  gulp.watch(globs.json, watchOptions, json);
  gulp.watch(globs.less, watchOptions, less);
  gulp.watch(globs.wxss, watchOptions, wxss);
  gulp.watch(globs.image, watchOptions, image);
  gulp.watch(globs.wxml, watchOptions, wxml);
};
複製代碼

3.2.2 gulp.lastRun實現增量編譯

在任何構建工具中增量編譯都是一個必不可少的優化方式。即在編譯過程當中僅編譯那些修改過的文件,能夠減小許多沒必要要的資源消耗,也能減小編譯時長gulp 4發佈以前社區早早給出了一系列的解決方案,gulp-changedgulp-cachedgulp-remembergulp-newer 等等。gulp 4發佈自帶了增量更新的方案gulp.lastRun()

gulp.lastRun 方法返回當前運行進程中成功完成 task 的最後一次時間戳。將其做爲 gulp.src 方法的參數 since 傳入,將每一個文件的mtime(文件內容最後被修改的時間)與since傳入的值進行比較,可實現跳過自上次成功完成任務以來沒有更改的文件,實現增量編譯,加快執行時間。使用方法以下所示:

/* 以 ts / less 爲例, js / json / wxss / copy / image 同理 */
const ts = () => gulp.src(
    'src/**/*.ts',
    { since: gulp.lastRun(ts) }
)...

const less = () => gulp.src(
    'src/**/*.less',
    { since: gulp.lastRun(less) }
)...
複製代碼

然而,該方法僅經過判別文件內容的修改來實現增量編譯,那對於未修改的文件的移動又該如何?好比將某個 js 文件複製到另一個目錄中,該文件的mtime(文件內容最後被修改的時間)是不會發生變化的,這時,ctime(寫入文件、更改全部者、權限或連接設置的最後修改時間)派上用場。代碼改造以下,封裝了since函數,當文件內容未變化,但文件路徑發生改變時,返回時間戳爲 0,將文件的mtime(文件內容最後被修改的時間)與since傳入的值(此時爲 0)對比,就能夠增量編譯該文件。

const since = task => file =>
  gulp.lastRun(task) > file.stat.ctime ? gulp.lastRun(task) : 0;

const ts = () => gulp.src(
    'src/**/*.ts',
    { since: since(ts) }
)
複製代碼

3.2.3 數據對比

增量編譯數據對比

3.3 開啓 sourcemap

gulp-sourcemaps這是一款用來生成映射文件的一個插件,SourceMap 文件記錄了一個存儲源代碼與編譯代碼對應位置映射的信息文件。咱們在調試時都是沒辦法像調試源碼般輕鬆,這就須要 SourceMap 幫助咱們在控制檯中轉換成源碼,從而進行 debuggulp-sourcemaps主要用於解決代碼混淆、typescriptless語言轉換編譯成jscss語言的問題。

使用 gulp-sourcemaps 插件,可爲參與編譯的 .ts.less 文件開啓 Source Map :

const sourcemaps = require('gulp-sourcemaps');

/* 以 ts 爲例, less 同理 */
const ts = () => gulp.src('src/**/*.ts')
    .pipe(sourcemaps.init())
    .pipe(tsProject())  // 編譯ts
    .pipe(mpNpm())      // 分析提取 ts 中用到的依賴
    .pipe(sourcemaps.write('.'))   // 以 .map 文件形式導出至同級目錄
    .pipe(gulp.dest('dist'));
複製代碼

注意:Source Map 文件不計入代碼包大小計算,即編譯上傳的代碼是不計算這部分體積的。開發版代碼包中因爲包含了 .map 文件,實際代碼包大小會比體驗版和正式版大。

3.4 編譯 ts

編譯 ts 的目標是將.ts文件轉換編譯成.js文件輸出到目標文件夾。主要分爲如下幾個步驟:

  1. 建立一個流,用於從文件系統讀取 Vinyl 對象。設置since屬性,利用gulp.lastRun進行增量編譯。
  2. 引入gulp-ts-alias插件處理路徑別名問題,該插件根據tsconfig.json文件中paths屬性的的配置內容,將別名替換爲原路徑。好比paths有一條配置以下"@/*": ["src/supermarket/*"],那麼對於任意.ts文件中的import A from '@/components'都會替換成import A from 'src/supermarket/components'
  3. dev 模式下開啓 sourcemap, 引入gulp-if來進行條件判斷,當config.sourcemap值爲 true 時,才執行sourcemaps.init(),不然流直接通向下一個 pipe。
  4. 引入gulp-typescript插件將.ts文件轉換編譯成.js文件。首先,在ts這個task外面使用gulpTs.createProject建立一個 ts 編譯任務,之因此在外面建立的緣由是當運行watch時,有.ts文件進行修改,須要從新構建ts這個task,在外面建立能夠節省一半的時間。在默認配置下,有 ts 的編譯錯誤會輸出到控制檯,而且編譯器會由於編譯錯誤而使這個構建任務崩潰,中斷運行。因此須要在tsProject()後面添加一個錯誤處理程序來捕獲錯誤。
  5. 引入gulp-mp-npm插件提取.ts文件引入的 npm 包。
  6. sourcemap 寫入到目標目錄。
  7. build 模式下引入gulp-uglify.js文件進行壓縮。
  8. 將編譯完成的.js文件輸出到對應目錄。
const gulpTs = require('gulp-typescript');
const tsAlias = require('gulp-ts-alias');
const gulpIf = require('gulp-if');
const uglifyjs = require('uglify-js');
const composer = require('gulp-uglify/composer');
const minify = composer(uglifyjs, console);
const pump = require('pump');

const tsProject = gulpTs.createProject(resolve('tsconfig.json'));   // 4. 外部建立一個ts編譯任務

const ts = cb => {
  const tsResult = gulp
    .src(globs.ts, { ...srcOptions, since: since(ts) }) // 1. 增量編譯
    .pipe(tsAlias({ configuration: tsProject.config })) // 2. 將路徑別名替換爲原路徑
    .pipe(gulpIf(config.sourcemap, sourcemaps.init()))  // 3. dev模式開啓sourcemap
    .pipe(tsProject())     // 4. 編譯ts
    .on('error', () => {    // 4. 捕獲錯誤,不添加會由於ts編譯錯誤致使任務中斷
      /** 忽略編譯器錯誤**/
    });

  pump(
    [
      tsResult.js,
      mpNpm(mpNpmOptions),     // 5. 分析依賴
      gulpIf(config.sourcemap.ts, sourcemaps.write('.')), // 6. 寫入sourcemap文件到對應的目錄
      gulpIf(config.compress, minify({})),     // 7. build模式壓縮js
      gulp.dest(dist),   // 8. 輸出文件到目標目錄
    ],
    cb,
  );
};
複製代碼

眼尖的讀者應該注意到了task的後半段改用pump將流連接在一塊兒。Pump是一個小型節點模塊,可將流鏈接在一塊兒並在其中任何一個關閉時將其所有銷燬。通俗來說,就是可使咱們更容易定位錯誤的發生點,經常使用來替代pipe,很是適合於修復gulp-uglify報錯。以下圖所示,使用pump後的報錯能準肯定位到具體的位置,而pipe拋出整個調用棧,讓人無從下手。

pipe和pump的區別

3.5 編譯 less

編譯 less 的目標是將.less文件轉換編譯成.wxss文件輸出到目標文件夾。主要分爲如下幾個步驟:

const gulpLess = require('gulp-less');
const weappAlias = require('gulp-wechat-weapp-src-alisa');
const prettyData = require('gulp-pretty-data');

const less = cb => {
  pump(
    [
      gulp.src(globs.less, { ...srcOptions, since: since(less) }), // 1. 增量編譯
      gulpIf(config.sourcemap.less, sourcemaps.init()), // 2. 開啓sourcemap
      weappAlias(weappAliasConfig), // 3. 將路徑別名替換成原路徑
      /** 此處省略 步驟A */
      gulpLess(), // 4. 編譯less轉換成css
      /** 此處省略 步驟B */
      rename({ extname: '.wxss' }), // 5. 文件.less後綴修改成.wxss
      mpNpm(mpNpmOptions), // 6. 依賴分析
      gulpIf(config.sourcemap.less, sourcemaps.write('.')), // 7. 寫入sourcemap
      gulpIf(
        config.compress,  // 8. build模式下壓縮wxss
        prettyData({
          type: 'minify',
          extensions: {
            wxss: 'css',
          },
        }),
      ),
      gulp.dest(dist), // 9. 輸出文件到目標目錄
    ],
    cb,
  );
};
複製代碼

如上所示,使用gulp-less插件將LESS代碼編譯成CSS代碼,他的編譯原理以下所示:

  • index.less文件引入variable.less變量文件,會將variable.less的內容複製到index.less文件中。
  • index.less文件引入style.less純樣式文件,會將style.less所有的內容複製到index.less文件中。
  • .less 文件編譯,將使用到的變量替換成對應的值;樣式嵌套平鋪。
  • 清空style.lessvariable.less文件的內容。
less編譯原理

設想一下,假設有 100 個文件引入style.less純樣式文件,那麼style.less的內容便要複製一百份。對於複雜的工程項目,less 文件的依賴是十分複雜的,編譯的結果是形成許多冗餘的樣式代碼,與咱們「精益求精」(儘量地壓縮小程序包)的理念背道而馳。基於此,咱們能夠在gulp-less插件編譯前,將@import **相關的代碼註釋掉,gulp-less插件編譯後,恢復註釋的內容,並將引入路徑的.less後綴修改成.wxss後綴,代碼以下所示:

/** 前面代碼片斷省略的步驟A代碼 */
tap(file => {
  const content = file.contents.toString(); // 將文件內容toString()
  const regNotes = /\/\*(\s|.)*?\*\//g;   // 匹配 /* */ 註釋
  const removeComment = content.replace(regNotes, ''); // 刪除註釋內容
  const reg = /@import\s+['|"](.+)['|"];/g; // 匹配 @import ** 路徑引入

  const str = removeComment.replace(reg, ($1, $2) => {
    const hasFilter = cssFilterFiles.filter(item => $2.indexOf(item) > -1);  // 過濾掉變量文件引入
    let path = hasFilter <= 0 ? `/** less: ${$1} **/` : $1;  // 將純樣式文件的引入 添加註釋 /** less: ${$1} **/
    return path;
  });
  file.contents = Buffer.from(str, 'utf8'); // string恢復成文件流
});
複製代碼

這裏須要注意的是,若是註釋掉變量文件,好比上述提到的variable.less文件,那麼引入的變量例如@color-primary就會取不到值,致使編譯出錯。所以處理時,能夠寫一個數組cssFilterFiles過濾掉變量文件,而後再註釋全部的樣式文件,好比上述提到的style.less純樣式文件。

執行步驟 A 代碼後,緊接着使用gulp-lessLESS代碼編譯成CSS代碼,以後在執行步驟 B 代碼,以下所示,將路徑註釋還原,並將引入路徑的.less後綴修改成小程序能識別的.wxss後綴。

/** 前面代碼片斷省略的步驟B代碼 */
tap(file => {
  const content = file.contents.toString();
  const regNotes = /\/\*\* less: @import\s+['|"](.+)['|"]; \*\*\//g;
  const reg = /@import\s+['|"](.+)['|"];/g;
  const str = content.replace(regNotes, ($1, $2) => {
    let less = '';
    $1.replace(reg, $3 => (less = $3));
    return less.replace(/\.less/g, '.wxss');
  });
  file.contents = Buffer.from(str, 'utf8');
});
複製代碼

優化後的效果以下圖所示:

less編譯優化

3.6 圖片壓縮

小程序主包目前只支持最多 2M 的大小,而圖片一般是佔用空間最多的資源。由於在項目中對圖片大小進行壓縮頗有必要的。

使用 gulp-image 插件,可壓縮圖片大小且能保證畫質:

const gulpImage = require('gulp-image');
const cache = require('gulp-cache');

const image = () =>
  gulp
    .src(globs.image, { ...srcOptions, since: since(image) })
    .pipe(cache(gulpImage())) // 緩存壓縮後的圖片
    .pipe(gulp.dest(dist));
複製代碼

以下圖所示,是項目全部圖片壓縮的結果,平均能節省 50%左右的體積。

image圖片壓縮

3.7 其它文件編譯

.js.json.wxml.wxss編譯的主要原理是將源文件拷貝至目標文件目錄。代碼以下所示:

const prettyData = require('gulp-pretty-data');

const wxml = () =>
  gulp
    .src(globs.wxml, { ...srcOptions, since: since(wxml) }) // 1. 增量編譯
    .pipe(
      gulpIf(
        config.compress,    // 2. build模式下壓縮文件
        prettyData({
          type: 'minify',
          extensions: {
            wxml: 'xml',
          },
        }),
      ),
    )
    .pipe(gulp.dest(dist)); // 3. 輸出到對應目錄

// .json、.wxml、.wxss代碼參考如上
複製代碼

在微信開發者工具中,點擊右上角 詳情 --> 本地設置 --> 選中上傳代碼時自動壓縮樣式上傳代碼時自動壓縮混淆,那麼在小程序構建打包時會壓縮.wxss文件和.js文件,因此在實際的 gulp 構建工做流中,能夠不對.ts.js.less.wxss進行壓縮處理,但對於.wxml.json文件,可使用gulp-pretty-data插件對文件進行壓縮。

4. 最後

小程序開發最大的限制是主包大小必須控制在 2M 之內,經過本文的優化方法,將本來的小程序主包體積減少 11.9%,想要進一步壓縮體積,能夠考慮tree-sharking,分析代碼依賴關係,剔除未引用的代碼。但現有的gulp工做流架構模式是很難完美地實現這個功能的,tree-sharkingrollupwebpack等構建方案的特長,這也是爲啥有一部分紅熟的小程序框架如tarompvue會選擇webpack構建。其實,各有所長,全看取捨。

附上代碼連接: codesandbox.io/s/miniprogr…

相關文章
相關標籤/搜索