10分鐘快速精通rollup.js——前置學習之基礎知識篇

sam9831發表於2018-11-23

前言

本文是《10分鐘快速精通rollup.js——Vue.js原始碼打包過程深度分析》的前置學習教程,講解的知識點以理解Vue.js打包原始碼為目標,不會做過多地展開。教程將保持rollup.js系列教程的一貫風格,大部分知識點都將提供可執行的程式碼案例和實際執行的結果,讓大家通過教程就可以看到實現效果,省去親自上機測試的時間。

1. fs的基本應用

fs模組是Node.js提供一組檔案操作API,用於對系統檔案及目錄進行讀寫操作。

判斷資料夾是否存在

刪除dist目錄,建立src/vue/fs測試程式碼目錄和index.js測試程式碼:

rm -rf dist
mkdir -p src/vue/fs
touch src/vue/fs/index.js
複製程式碼

通過非同步和同步兩種方式判斷dist目錄是否存在:

const fs = require('fs')

fs.exists('./dist', result => console.log(result))

const exists = fs.existsSync('./dist')
if (exists) {
  console.log('dist目錄存在')
} else {
  console.log('dist目錄不存在')
}
複製程式碼

通過node執行程式碼:

$ node src/vue/fs/index.js

dist目錄不存在
false
複製程式碼

根據執行結果我們可以看到同步的任務先完成,而非同步的任務會延後一些,但是同步任務會導致主執行緒阻塞,在實際應用過程中需要根據實際應用場景進行取捨。

建立資料夾

通過非同步的方式建立dist目錄:

const fs = require('fs')

fs.exists('./dist', result => !result && fs.mkdir('./dist'))
複製程式碼

通過同步的方式建立dist目錄:

const fs = require('fs')

if (!fs.existsSync('./dist')) {
  fs.mkdirSync('./dist')
}
複製程式碼

檢查dist目錄是否生成:

$ ls -al
total 312
drwxr-xr-x    5 sam  staff    160 Nov 22 14:15 dist/
複製程式碼

讀取檔案

我們先通過rollup.js打包程式碼,在dist目錄下會生成index-cjs.js和index-es.js:

$ rollup -c
./src/plugin/main.js  ./dist/index-cjs.js, ./dist/index-es.js...
created ./dist/index-cjs.js, ./dist/index-es.js in 27ms
複製程式碼

通過非同步方式讀取index-cjs.js的內容,注意讀取到的檔案file是一個Buffer物件,通過toString()方法可以獲取到檔案的文字內容:

const fs = require('fs')

fs.readFile('./dist/index-cjs.js', (err, file) => {
  if (!err) console.log(file.toString()) // 列印檔案內容 
}) // 通過非同步讀取檔案內容
複製程式碼

通過同步方式讀取index-cjs.js的內容:

const fs = require('fs')

const file = fs.readFileSync('./dist/index-cjs.js') // 通過同步讀取檔案內容
console.log(file.toString()) // 列印檔案內容
複製程式碼

執行程式碼,可以看到成功讀取了檔案內容:

$ node src/vue/fs/index.js 
/**
 * ==============================
 * welcome to imooc.com
 * this is a rollup test project
 * ==============================
 **/
'use strict';

var a = Math.floor(Math.random() * 10);
var b = Math.floor(Math.random() * 100);

# ...
複製程式碼

寫入檔案

覆蓋寫入

通過非同步方式讀取src/vue/fs/index.js的內容,並寫入dist/index.js:

const fs = require('fs')

fs.readFile('./src/vue/fs/index.js', (err, file) => {
  if (!err) fs.writeFile('./dist/index.js', file, () => {
    console.log('寫入成功') // 寫入成功的回撥
  }) // 通過非同步寫入檔案
}) // 通過非同步讀取檔案
複製程式碼

通過同步方式實現與上面一樣的功能:

const fs = require('fs')

const code = fs.readFileSync('./src/vue/fs/index.js') // 同步讀取檔案
fs.writeFileSync('./dist/index.js', code) // 同步寫入檔案
複製程式碼

需要注意的是writeFile()方法預設情況下會覆蓋dist/index.js的內容,即先清空檔案再寫入。

追加寫入

很多時候我們需要在檔案末尾追加寫入一些內容,可以增加flag屬性進行標識,當flag的值為a時,表示追加寫入:

const fs = require('fs')

const code = fs.readFileSync('./src/vue/fs/index.js')
fs.writeFileSync('./dist/index.js', code, { flag: 'a' })
複製程式碼

驗證方法非常簡單,大家可以自己嘗試。

2. path的基本應用

path模組是Node.js提供的用於處理檔案路徑的函式集合。

生成絕對路徑

path.resolve()方法可以幫助我們生成絕對路徑,建立src/vue/path測試程式碼路徑和index.js測試程式碼:

mkdir -p src/vue/path
touch src/vue/path/index.js
複製程式碼

寫入如下測試程式碼:

const path = require('path')

console.log(path.resolve('./dist/index.js'))
console.log(path.resolve('src', 'vue/path/index.js'))
console.log(path.resolve('/src', '/vue/path/index.js'))
console.log(path.resolve('/src', 'vue/path/index.js'))
複製程式碼

測試程式碼執行結果:

$ node src/vue/path/index.js 

/Users/sam/WebstormProjects/rollup-test/dist/index.js
/Users/sam/WebstormProjects/rollup-test/src/vue/path/index.js
/vue/path/index.js
/src/vue/path/index.js
複製程式碼

通過測試結果不難看出path.resolve()的工作機制:

  • 從左往右依次拼裝路徑;
  • 如果引數為相對路徑,則會將相對路徑拼接起來,再加上當前目錄的絕對路徑合併成一個完整路徑;
  • 如果引數為絕對路徑,則會以最後一個引數的絕對路徑為準;
  • 如果引數既有絕對路徑也有相對路徑,則會按照從左向後的順序進行拼接。

生成相對路徑

在src/vue/path/index.js寫入如下程式碼:

const path = require('path')
const fs = require('fs')

const absolutePath = path.resolve('src', 'vue/path/index.js')
console.log(path.relative('./', absolutePath))
console.log(path.relative(absolutePath, './'))
複製程式碼

執行程式碼:

$ node src/vue/path/index.js 

src/vue/path/index.js
../../../..
複製程式碼

通過執行結果我們可以看到path.relative(a, b)方法提供了兩個引數,返回的結果是第一個引數到第二個引數的相對路徑,換句話說就是如何從第一個路徑到達第二個路徑:

  • 第一個案例中:第一個引數是專案的根目錄,第二個引數是./src/vue/path/index.js,所以第一個路徑到達第二個路徑的相對路徑是src/vue/path/index.js
  • 第二個案例中:第一個引數是./src/vue/path/index.js,第二個路徑是專案的根目錄,所以第一個路徑到達第二個路徑的相對路徑是../../../..

3. buble的基本應用

buble是什麼?

The blazing fast, batteries-included ES2015 compiler.

buble是一款類似babel的ES編譯器,它的主要特性如下:

  • 無配置,沒有plugins和preset的概念,可擴充套件性較低,但簡單易用。
  • 相對較小,速度更快。
  • 避免無法在ES2015中表達的程式碼,如for...of。buble不支援的功能列表:buble.surge.sh/guide/#unsu…

buble命令列模式

全域性安裝buble:

npm i -g buble
複製程式碼

建立buble的測試程式碼:

mkdir -p src/vue/buble
touch src/vue/buble/index.js
複製程式碼

在src/vue/buble/index.js中寫入以下內容:

const a = 1 // 使用ES6新語法:const
let b = 2 // 使用ES6新語法:let
const c = () => a + b // 使用ES6新特性:箭頭函式
console.log(a, b, c())
複製程式碼

使用buble編譯程式碼,並列印出結果:

$ buble src/vue/buble/index.js 

var a = 1
var b = 2
var c = function () { return a + b; }
console.log(a, b, c())
複製程式碼

相比babel,buble使用起來更加簡便,不再需要配置。但是bubble對某些語法是不支援的,比如for...of,我們修改src/vue/buble/index.js,寫入如下程式碼:

const arr = [1, 2, 3]
for (const value of arr) {
  console.log(value)
}
複製程式碼

使用node執行程式碼:

$ node src/vue/buble/index.js 

1
2
3
複製程式碼

程式碼可以正常執行,我們再通過buble編譯程式碼:

buble src/vue/buble/index.js 
---
1 : const arr = [1, 2, 3]
2 : for (const value of arr) {
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
for...of statements are not supported. Use `transforms: { forOf: false }` to skip transformation and disable this error, or `transforms: { dangerousForOf: true }` if you know what you're doing (2:0)
複製程式碼

可以看到buble提示for...of statements are not supported,所以使用buble之前一定要了解哪些語法不能被支援,以免出現編譯報錯。

buble API模式

除了命令列之外,我們還可以通過API來進行編譯,在程式碼中引入buble庫:

npm i -D buble
複製程式碼

在src/vue/buble目錄下建立buble-build.js檔案:

touch src/vue/buble/buble-build.js
複製程式碼

我們在buble-build.js檔案中寫入以下程式碼,在這段程式碼中我們通過fs模組獲取src/vue/buble/index.js檔案內容,應用buble的API進行編譯,編譯的關鍵方法是buble.tranform(code):

const buble = require('buble')
const fs = require('fs')
const path = require('path')

const codePath = path.resolve('./src/vue/buble/index.js') // 獲取程式碼的絕對路徑
const file = fs.readFileSync(codePath) // 獲取緩衝區檔案內容
const code = file.toString() // 將緩衝區檔案轉為文字格式
const result = buble.transform(code) // 通過buble編譯程式碼

console.log(result.code) // 列印buble編譯的程式碼
複製程式碼

通過node執行buble-build.js:

$ node src/vue/buble/buble-build.js 

var a = 1
var b = 2
var c = function () { return a + b; }
console.log(a, b, c())
複製程式碼

編譯成功!這裡需要注意的是buble.transfomr()方法傳入的引數必須是String型別,不能支援Buffer物件,如果將fs.readFileSync()獲取的Buffer物件直接傳入會引發報錯。

$ node src/vue/buble/buble-build.js 
/Users/sam/WebstormProjects/rollup-test/node_modules/_magic-string@0.25.1@magic-string/dist/magic-string.cjs.js:187
        var lines = code.split('\n');
                         ^

TypeError: code.split is not a function
複製程式碼

4. flow的基本應用

Flow is a static checker for javascript.

flow是Javascript靜態程式碼型別檢查器,Vue.js應用flow進行型別檢查。

應用flow靜態型別檢查

我們在程式碼中引入flow:

npm i -D flow-bin
複製程式碼

修改package.json,在scripts中新增flow指令:

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "flow": "flow"
  }
}
複製程式碼

在程式碼根路徑下執行以下指令,進行flow專案初始化:

$ npm run flow init

> rollup-test@1.0.0 flow /Users/sam/WebstormProjects/rollup-test
> flow "init"
複製程式碼

此時會在專案根路徑下生成.flowconfig檔案,接下來我們嘗試執行flow進行程式碼型別的靜態檢查:

$ npm run flow

> rollup-test@1.0.0 flow /Users/sam/WebstormProjects/rollup-test
> flow

Launching Flow server for /Users/sam/WebstormProjects/rollup-test
Spawned flow server (pid=24734)
Logs will go to /private/tmp/flow/zSUserszSsamzSWebstormProjectszSrollup-test.log
Monitor logs will go to /private/tmp/flow/zSUserszSsamzSWebstormProjectszSrollup-test.monitor_log
No errors!
複製程式碼

接下來我們建立flow的測試檔案:

mkdir -p src/vue/flow
touch src/vue/flow/index.js
複製程式碼

先看一個官方提供的例子,在src/vue/flow/index.js中寫入如下程式碼:

/* @flow */ // 指定該檔案flow檢查物件
function square(n: number): number { // square的引數必須為number型別,返回值必須為number型別
  return n * n
}

console.log(square("2"))
複製程式碼

flow只會檢查程式碼頂部新增了/* @flow */// flow的原始碼。這裡square("2")方法傳入的引數是string型,與我們定義的型別不相符,執行flow進行型別檢查:

$ npm run flow

> rollup-test@1.0.0 flow /Users/sam/WebstormProjects/rollup-test
> flow

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/vue/flow/index.js:6:8

Cannot call square with "2" bound to n because string [1] is
incompatible with number [2].
複製程式碼

flow不僅檢查出了錯誤,還能精確定位到出錯位置。我們將程式碼修改為正確型別:

/* @flow */
function square(n: number): number {
  return n * n
}

console.log(square(2))
複製程式碼

此時我們嘗試用node執行src/vue/flow/index.js:

$ node src/vue/flow/index.js 
/Users/sam/WebstormProjects/rollup-test/src/vue/flow/index.js:2
function square(n: number): number {
                 ^

SyntaxError: Unexpected token :
複製程式碼

可以看到程式碼無法直接執行,因為node不能識別型別檢查,這時我們可以通過babel-node來實現flow程式碼的執行,首先安裝babel的flow外掛:

npm i -D @babel/plugin-transform-flow-strip-types
複製程式碼

修改.babelrc配置檔案,增加flow外掛的支援:

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    "@babel/plugin-transform-flow-strip-types"
  ]
}
複製程式碼

嘗試babel-node執行程式碼:

$ babel-node src/vue/flow/index.js 
4
複製程式碼

得到了正確的結果,這得益於babel的flow外掛幫助我們消除flow檢查部分的程式碼,使得程式碼可以正常執行。

自定義型別檢查

flow的強大之處在於可以進行自定義型別檢查,我們在專案的根目錄下建立flow資料夾,並新增test.js檔案:

mkdir -p flow
touch flow/test.js
複製程式碼

在test.js中寫入如下內容:

declare type Test = {
  a?: number;
  b?: string;
  c: (key: string) => boolean;
}
複製程式碼

declare type表示宣告一個自定義型別,這個配置檔案的具體含義如下:

  • 物件中屬性a的型別為number,該屬性可以為空(?表示該屬性可以為空);
  • 物件中屬性b的型別為string,該屬性可以為空;
  • 物件中屬性c的型別為function,只能包含一個引數,型別為string,返回值為boolean,注意c不能為空,也就是說如果指定一個物件的型別為Test,那麼這個物件中必須包含一個名稱為c的屬性。flow強大之處在於不僅可以指定型別,還能規定屬性的名稱,確保程式碼的一致性。

接下來我們修改.flowconfig,在[libs]下新增flow,這樣flow在初始化時會前往專案根目錄下的flow資料夾中尋找並載入自定義型別:

[ignore]

[include]

[libs]
flow

[lints]

[options]

[strict]
複製程式碼

接著我們在src/vue/flow下建立type.test.js檔案:

touch src/vue/flow/type-test.js
複製程式碼

寫入如下程式碼,對自定義型別進行測試:

/* @flow */
const obj : Test = {
  a: 1,
  b: 'b',
  c: (p) => {
	  return new String(p) instanceof String
  }
}
console.log(obj.c("c"))
複製程式碼

通過flow指令進行靜態檢查,並通過babel-node執行程式碼:

$ npm run flow
> rollup-test@1.0.0 flow /Users/sam/WebstormProjects/rollup-test
> flow

No errors!

$ babel-node src/vue/flow/type-test.js 
true
複製程式碼

如果程式碼中obj物件不定義c屬性:

/* @flow */
const obj : Test = {
  a: 1,
  b: 'b'
}
複製程式碼

執行flow後會出現報錯:

$ npm run flow

> rollup-test@1.0.0 flow /Users/sam/WebstormProjects/rollup-test
> flow

Please wait. Server is initializing (parsed files 3000): -^[[2A^[[Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/vue/flow/type-test.js:2:20

Cannot assign object literal to obj because property c is missing
in object literal [1] but exists in Test [2].
複製程式碼

5. zlib的基本應用

zlib是Node.js的內建模組,它提供通過Gzip和Deflate/Inflate實現的壓縮功能。

Vue.js原始碼編譯時僅用到zlib.gzip()方法,先了解一下gzip的用法:

zlib.gzip(buffer[, options], callback)
複製程式碼

引數的含義如下:

  • buffer:需要壓縮檔案的buffer
  • options:引數,可以為空
  • callback:壓縮成功後的回撥函式

建立src/vue/zlib目錄,並建立index.js檔案,用於zlib測試:

mkdir -p src/vue/zlib
touch src/vue/zlib/index.js
複製程式碼

嘗試通過fs模組讀取dist/index-cjs.js檔案的內容,並通過gzip進行壓縮:

const fs = require('fs')
const zlib = require('zlib')

fs.readFile('./dist/index-cjs.js', (err, code) => {
  if (err) return
  console.log('原檔案容量:' + code.length)
  zlib.gzip(code, (err, zipped) => {
    if (err) return
    console.log('gzip壓縮後容量:' + zipped.length)
  })
})
複製程式碼

通過node執行程式碼:

$ node src/vue/zlib/index.js 

原檔案容量:657
gzip壓縮後容量:329
複製程式碼

值得注意的是,傳入buffer進行壓縮與傳入string進行壓縮的結果是完全一致的。我們修改程式碼:

fs.readFile('./dist/index-cjs.js', (err, code) => {
  if (err) return
  console.log('原檔案容量:' + code.toString().length)
  zlib.gzip(code.toString(), (err, zipped) => {
    if (err) return
    console.log('gzip壓縮後容量:' + zipped.length)
  })
})
複製程式碼

再次執行,可以看到同樣的結果:

$ node src/vue/zlib/index.js 

原檔案容量:657
gzip壓縮後容量:329
複製程式碼

所以結論是無論通過buffer還是string獲取的length都是一致的,通過buffer和string進行gzip壓縮後獲得的結果也是一致的。

6. terser的基本應用

A JavaScript parser, mangler/compressor and beautifier toolkit for ES6+.

為什麼選擇terser

terser是一個Javascript程式碼的壓縮和美化工具,選擇terser的原因有兩點:

  • uglify-es不再維護,而uglify-js不支援 ES6+,這一點在上一篇教程中我們已經看到了,rollup-plugin-uglify就是基於uglify-js,所以它不能夠支援ES6語法;
  • terser是uglify-es的一個分支,它保持了與uglify-es和uglify-js@3的API及CLI的相容。

terser命令列模式:

全域性安裝terser:

npm i -g terser
複製程式碼

通過terser壓縮檔案:

terser dist/index-cjs.js
複製程式碼

如果先輸入引數再輸入檔案,建議增加雙短劃線(--)進行分割:

terser -c -m -o dist/index-cjs.min.js -- dist/index-cjs.js
複製程式碼

各引數的含義如下:

  • -c / --compress:對程式碼格式進行壓縮
  • -m / --mangle:對變數名稱進行壓縮
  • -o / --output:指定輸出檔案路徑

對比壓縮結果,普通壓縮:

$ terser dist/index-cjs.js

"use strict";var a=Math.floor(Math.random()*10);var b=Math.floor(Math.random()*100);function random(base){if(base&&base%1===0){return Math.floor(Math.random()*base)}else{return 0}}var test=Object.freeze({a:a,b:b,random:random});const a$1=1;const b$1=2;console.log(test,a$1,b$1);var main=random;module.exports=main;
複製程式碼

可以看到程式碼間的空格被去除,程式碼結構更加緊湊。下面加入-c引數後再次壓縮:

$ terser dist/index-cjs.js -c

"use strict";var a=Math.floor(10*Math.random()),b=Math.floor(100*Math.random());function random(base){return base&&base%1==0?Math.floor(Math.random()*base):0}var test=Object.freeze({a:a,b:b,random:random});const a$1=1,b$1=2;console.log(test,1,2);var main=random;module.exports=main;
複製程式碼

加入-c後,產生如下幾個變化:

  • 變數定義更加緊湊;
  • 將if判斷改為了三目運算子;
  • console.log時直接將變數替換為值。

下面我們使用-m引數再對比一下:

$ terser dist/index-cjs.js -m

"use strict";var a=Math.floor(Math.random()*10);var b=Math.floor(Math.random()*100);function random(a){if(a&&a%1===0){return Math.floor(Math.random()*a)}else{return 0}}var test=Object.freeze({a:a,b:b,random:random});const a$1=1;const b$1=2;console.log(test,a$1,b$1);var main=random;module.exports=main;
複製程式碼

加入-m後,主要修改了變數的名稱,如random函式中的形參base變成了a,同時加入-m和-c後程式碼變得更加精簡:

$ terser dist/index-cjs.js -m -c

"use strict";var a=Math.floor(10*Math.random()),b=Math.floor(100*Math.random());function random(a){return a&&a%1==0?Math.floor(Math.random()*a):0}var test=Object.freeze({a:a,b:b,random:random});const a$1=1,b$1=2;console.log(test,1,2);var main=random;module.exports=main;
複製程式碼

terser API模式

我們可以通過API進行程式碼壓縮,這也是Vue.js採用的方法,在專案中安裝terser模組:

npm i -D terser
複製程式碼

建立src/vue/terser目錄,並建立index.js檔案:

mkdir -p src/vue/terser
touch src/vue/terser/index.js
複製程式碼

在src/vue/terser/index.js中寫入如下程式碼,我們嘗試通過fs模組讀取dist/index-cjs.js檔案內容,並通過terser進行壓縮,這裡關鍵的方法是terser.minify(code, options)

const fs = require('fs')
const terser = require('terser')

const code = fs.readFileSync('./dist/index-cjs.js').toString() // 同步讀取程式碼檔案
const minifyCode = terser.minify(code, { // 通過terser.minify進行最小化壓縮
  output: {
    ascii_only: true // 僅支援ascii字元,非ascii字元將轉成\u格式
  },
  compress: {
    pure_funcs: ['func'] // 如果func的返回值沒有被使用,則進行替換
  }
})

console.log(minifyCode.code)
複製程式碼

我們修改src/plugin/main.js的原始碼,用於壓縮測試:

import * as test from 'sam-test-data'

const a = 1
const b = 2
console.log(test, a, b)
function func() {
  return 'this is a function'
}
func() // 使用func()函式,但沒有利用函式返回值,用於測試compress的pure_funcs引數
console.log('') // 加入非ascii字元,用於測試output的ascii_only引數
export default test.random
複製程式碼

修改rollup.config.js配置檔案,這裡值得注意的是我們加入了treeshake:false的配置,因為預設情況下冗餘程式碼會被rollup.js剔除,這樣我們就無法測試terser壓縮的效果了:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'

export default {
  input: './src/plugin/main.js',
  output: [{
    file: './dist/index-cjs.js',
    format: 'cjs'
  }],
  plugins: [
    resolve(),
    commonjs(),
    babel()
  ],
  treeshake: false // 關閉tree-shaking特性,將不再自動刪除冗餘程式碼
}
複製程式碼

應用rollup.js進行打包,並對打包後的程式碼進行壓縮:

$ rollup -c

./src/plugin/main.js  ./dist/index-cjs.js...
created ./dist/index-cjs.js in 436ms

$ node src/vue/terser/index.js 

"use strict";var a=Math.floor(10*Math.random()),b=Math.floor(100*Math.random());function random(a){return a&&a%1==0?Math.floor(Math.random()*a):0}var test=Object.freeze({a:a,b:b,random:random}),a$1=1,b$1=2;function func(){return"this is a function"}console.log(test,a$1,b$1),console.log("\ud83d\ude01\ud83d\ude01");var main=random;module.exports=main;
複製程式碼

檢視壓縮後的檔案,發現我們的配置生效了:

  • 單獨呼叫的func()被刪除,這個配置我們可以用於壓縮時刪除日誌列印,配置方法如下:
compress: {
	pure_funcs: ['func', 'console.log']
}
複製程式碼
  • 非ascii字元被替換:“”被替換為“\ud83d\ude01\ud83d\ude01”

總結

本教程主要講解了以下知識點:

  • fs模組:Node.js內建模組,用於本地檔案系統處理;
  • path模組:Node.js內建模組,用於本地路徑解析;
  • buble模組:用於ES6+語法編譯;
  • flow模組:用於Javascript原始碼靜態檢查;
  • zlib模組:Node.js內建模組,用於使用gzip演算法進行檔案壓縮;
  • terser模組:用於Javascript程式碼壓縮和美化。

相關文章