衆所周知,小程序主包的大小限制在 2M 之內,對於日益龐大的工程項目,開發者們無所不用其極地進行優化。原生小程序基本採用gulp
進行構建,可否在gulp
構建流程中作文章,達到「優化」的目的。css
首先明確gulp
構建優化的目的是壓縮小程序包體積,提高用戶開發體驗。做者在熟悉gulp
開發後,推翻了原有的構建流程,從新設計,本着精益求精的目的進行優化,取得以下三點進展:vue
工具
--> 構建 npm
,生成小程序專用的npm
包才能成功編譯代碼,同時經常會由於各類路徑引入問題致使不得不從新編輯或者從新構建 npm。優化後再無此類煩惱,gulp 構建時自動分析生成小程序專用的npm
包,大大提升了開發效率。對小程序來講,除了app.js
做爲程序入口以外,每一個 page 頁面均可以做爲一個頁面入口,更傾向是固定路徑模式的多頁應用。gulp 構建的目的是將開發路徑的代碼翻譯轉到小程序專用路徑,該路徑下的代碼可以被微信開發者工具讀取、編譯、構建。經過 gulp 工具可實現:node
.ts
文件編譯爲 .js
、 .less
文件編譯爲 .wxss
,以支持 TypeScript
、 Less
語法。sourcemaps
方便錯誤調試與定位。npm
包與小程序專用 npm
包。watch
,方便開發者調試。採用原生框架開發的小程序主要有.js
、.json
、.wxml
、.wxss
四種文件構成,爲了提高開發效率,一般會引入.ts
和.less
文件。因爲每種文件的編譯構建方法不盡相同,所以須要爲不一樣類型的文件建立不一樣的task
。webpack
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 => {}; // 除上述文件外的其它文件複製到目標文件夾
複製代碼
如約定俗成通常,咱們一般在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, // 添加監聽
);
複製代碼
前面的篇幅講述了 gulpfile 文件總體架構設計,下面講述每一個task
的具體配置,以及優化之道。github
① 安裝 npm 包web
安裝 npm 包的方法有如下兩種:typescript
package.json
所在的目錄中執行命令npm install
安裝 npm 包,此處要求參與構建 npm 的 package.json
須要在 project.config.js
定義的 miniprogramRoot
以內。gulp
的task
進行處理,建立一個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 包。 json
如上圖所示,小程序真正使用的 npm 包和node_modules
目錄下的 npm 包的結構是有差別的,這個差別就是點擊構建 npm
這個操做執行的打包過程,分爲兩種:小程序 npm 包會直接拷貝構建文件生成目錄下的全部文件到 miniprogram_npm
中;其餘 npm 包則會從入口 js 文件開始走一遍依賴分析和打包過程(相似 webpack
)。
顯而易見,官方提供的 npm 構建方案暴露了如下兩點問題:
gulp
打包構建小程序時,第一個task
就是在miniprogram
目錄安裝 npm 包,當依賴的 npm 包數量夠多時,會消耗大量的時間。工具
--> 構建 npm
,對開發者十分不友好。所以咱們但願能借助gulp
工做流,在小程序構建時分析每一個文件的依賴關係,將須要的 npm 包拷貝至 miniprogram_npm
目錄。一步到位,直接省略了上述安裝npm包
和構建npm包
兩個步驟。
所幸,社區無所不能,有做者開發了一個用以小程序提取 npm 依賴包的 gulp
插件 gulp-mp-npm
,有如下特色:
前面提到過構建npm包
時,針對小程序 npm 包會直接拷貝構建文件生成目錄下的全部文件到 miniprogram_npm
中;對於其它普通 npm 包則會從入口 js 文件開始走一遍依賴分析和打包過程(相似 webpack
),打包生成的代碼在同級目錄下會生成 source map
文件,方便進行逆向調試。而gulp-mp-npm
插件僅僅經過依賴分析,提取使用到的依賴和組件,將其複製到至 miniprogram_npm
目錄下對應的 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 小程序組件。
衆所周知,gulp
的task
都是單次執行的。某個文件變化後 gulp.watch
會從新執行整個 task
來完成構建,這樣會致使未變化的文件重複構建,效能較低。能夠採起如下兩個措施進行效率優化:
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);
};
複製代碼
gulp.lastRun
實現增量編譯在任何構建工具中增量編譯都是一個必不可少的優化方式。即在編譯過程當中僅編譯那些修改過的文件,能夠減小許多沒必要要的資源消耗,也能減小編譯時長。gulp 4
發佈以前社區早早給出了一系列的解決方案,gulp-changed
、gulp-cached
、gulp-remember
、gulp-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) }
)
複製代碼
gulp-sourcemaps
這是一款用來生成映射文件的一個插件,SourceMap 文件記錄了一個存儲源代碼與編譯代碼對應位置映射的信息文件。咱們在調試時都是沒辦法像調試源碼般輕鬆,這就須要 SourceMap
幫助咱們在控制檯中轉換成源碼,從而進行 debug
。gulp-sourcemaps
主要用於解決代碼混淆、typescript
和less
語言轉換編譯成js
和css
語言的問題。
使用 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
文件,實際代碼包大小會比體驗版和正式版大。
編譯 ts 的目標是將.ts
文件轉換編譯成.js
文件輸出到目標文件夾。主要分爲如下幾個步驟:
Vinyl
對象。設置since
屬性,利用gulp.lastRun
進行增量編譯。gulp-ts-alias
插件處理路徑別名問題,該插件根據tsconfig.json
文件中paths
屬性的的配置內容,將別名替換爲原路徑。好比paths
有一條配置以下"@/*": ["src/supermarket/*"]
,那麼對於任意.ts
文件中的import A from '@/components'
都會替換成import A from 'src/supermarket/components'
。gulp-if
來進行條件判斷,當config.sourcemap
值爲 true 時,才執行sourcemaps.init()
,不然流直接通向下一個 pipe。gulp-typescript
插件將.ts
文件轉換編譯成.js
文件。首先,在ts
這個task
外面使用gulpTs.createProject
建立一個 ts 編譯任務,之因此在外面建立的緣由是當運行watch
時,有.ts
文件進行修改,須要從新構建ts
這個task
,在外面建立能夠節省一半的時間。在默認配置下,有 ts 的編譯錯誤會輸出到控制檯,而且編譯器會由於編譯錯誤而使這個構建任務崩潰,中斷運行。因此須要在tsProject()
後面添加一個錯誤處理程序來捕獲錯誤。gulp-mp-npm
插件提取.ts
文件引入的 npm 包。sourcemap
寫入到目標目錄。gulp-uglify
對.js
文件進行壓縮。.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
拋出整個調用棧,讓人無從下手。
編譯 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.less
和variable.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-less
將LESS
代碼編譯成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');
});
複製代碼
優化後的效果以下圖所示:
小程序主包目前只支持最多 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%左右的體積。
.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
插件對文件進行壓縮。
小程序開發最大的限制是主包大小必須控制在 2M 之內,經過本文的優化方法,將本來的小程序主包體積減少 11.9%,想要進一步壓縮體積,能夠考慮tree-sharking
,分析代碼依賴關係,剔除未引用的代碼。但現有的gulp
工做流架構模式是很難完美地實現這個功能的,tree-sharking
是rollup
、webpack
等構建方案的特長,這也是爲啥有一部分紅熟的小程序框架如taro
、mpvue
會選擇webpack
構建。其實,各有所長,全看取捨。
附上代碼連接: codesandbox.io/s/miniprogr…