【轉載】萌新也能懂的現代 JavaScript 開發
有時候入門沒入好,學習起現代的 JavaScript 開發還是頗有難度的。生態系統太日新月異以至於讓人難以理解不同工具的出現究竟是為了解決什麼問題。我從 1988 年開始程式設計,但直至 2014 年才開始認真地學習起 JavaScript。記得那時第一次遇到 Browserify 時看著它的標語:
Browserify lets you require(‘modules’) in the browser by bundling up all of your dependencies.
這句話裡的單詞我一個都看不懂,不知道為什麼這個東西會對自己這樣一個開發者能有用。
本篇文章的目標便是提供一個有關 JavaScript 工具進化的歷史視角,看看它們是怎麼一步一步演變到如今 — 2017 年的這個樣子的。開始的開始,我們將像最原始的恐龍家族一樣建一個網站 — 不依賴任何工具,就用最簡單的 HTML 和 JavaScript。然後再循序漸進引入不同的工具,看看每個工具分別解決了什麼問題。開始吧~
“守舊派”使用 JavaScript 的方法
首先,來像“守舊派”一樣使用 HTML 和 JavaScript 來建個網站 — 手動下載和內聯檔案。像這樣~
<!-- index.html --><!DOCTYPE html> <htmllang="en"><head><metacharset="UTF-8"><title>JavaScript Example</title><scriptsrc="index.js"></script></head><body><h1>Hello from HTML!</h1></body></html>
<script src="index.js"></script> 這行指的是引入在同一目錄下的一個叫做 index.js 的單獨檔案:
// index.jsconsole.log("Hello from JavaScript!");
這幾乎就是你建站需要的一切了~ 現在呢,你想加進去一個其他人寫的庫,例如 moment.js (一個以可讀性更好的方式格式化日期的庫)。例如,你可以像這樣使用一個 moment 函式:
moment().startOf('day').fromNow();// 20 hours ago
但這是假設你已經在網站中包括了 moment.js 的前提下!在 moment.js 的主頁上,你可以看到如下的指南:
Hmm,在 Install 這部分右邊貌似有很多東西啊!先無視它 — 我們可以下載 moment.min.js 檔案到同級目錄中,並在 index.html 中引入:
<!-- index.html --><!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Example</title><linkrel="stylesheet"href="index.css"><scriptsrc="moment.min.js"></script><scriptsrc="index.js"></script></head><body><h1>Hello from HTML!</h1></body></html>
注意 moment.min.js 會在 index.js 之前下載完成,所以你可以在 index.js 中像這樣使用 moment 函式:
// index.jsconsole.log("Hello from JavaScript!");console.log(moment().startOf('day').fromNow());
這就是我們怎麼利用 JavaScript 庫建站! 這種方式的好處是非常容易理解。然而壞處是當庫的作者更新時,每次尋找和下載新的版本就會很煩。
使用 JavaScript 包管理工具(npm)
大約從 2010 年開始,幾個相互競爭的 JavaScript 包管理工具開始出現,來幫人們從中心倉庫中自動完成下載和升級的流程。Bower 在 2013 年可以說是最流行的一個了,但最終在 2015 年左右還是被 npm 打敗了。(值得一提的是,大約從 2016 年開始,yarn 作為一個可選的 npm 替代開始佔據一席之地,但 npm 仍佔上風。)
注意到,npm 作為一個包管理工具,最開始是專門為 node.js — 一個執行在伺服器端的 JavaScript 執行時 (runtime) 而準備的,並不是前端。所以選擇它作為在瀏覽器中執行 JavaScript 的前端包管理工具似乎有些奇怪。
注意: 使用包管理工具通常涉及命令列的使用,在以前這並不是一個前端開發者所必須的技能。如果你從未使用過,可以閱讀這篇教程概覽一番。無論如何,知道如何使用命令列也是現代 JavaScript 開發的一部分(這也是進入其他領域重要的一步)。
讓我們看看怎麼使用 npm 來自動完成 moment.js 的安裝。如果你安裝了 node.js,npm 也就已經隨之安裝了,你可以操縱你的命令列進入 index.html 目錄下並輸入:
$ npm init
這將會引導你填寫幾個問題(預設值也可以,你可以一直敲 “Enter” 鍵)並生成一個叫做 package.json 的檔案,大概長這樣:
{"name":"your-project-name","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC"}
要安裝 monment.js 包的話,我們可以按照主頁上介紹的 npm 安裝方法在命令列敲下:
$ npm install moment --save
這行命令做了兩件事 — 第一,它從 moment.js package 下載了所有程式碼到一個叫 node_modules 的資料夾中。第二,它自動修改了 package.json 檔案來追蹤 moment.js 作為專案的依賴:
{"name":"modern-javascript-example","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC","dependencies":{// 手動加粗"moment":"^2.19.1"}}
這在和他人共享專案時非常有用 — 並不需要直接共享 node_modules 資料夾(可能會非常大),你只需要共享你的 package.json 檔案,其他開發者便可以使用命令 npm install 自動安裝專案所需要的包。
現在,我們不再需要從網站上手動下載 moment.js 了,我們可以使用 npm 自動下載和更新。然後看一下 node_modules 內部,發現 moment.min.js 檔案位於 node_modules/moment/min 目錄。所以可以把 npm 下載的版本的 moment.min.js 像這樣引入 index.html 檔案:
<!-- index.html --><!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>JavaScript Example</title><!-- 手動加粗下一行 --><scriptsrc="node_modules/moment/min/moment.min.js"></script><scriptsrc="index.js"></script></head><body><h1>Hello from HTML!</h1></body></html>
所以好訊息是我們現在可以通過命令列使用 npm 下載和更新包了。壞訊息是我們還得搜尋整個 node_modules 目錄來定位每個包的路徑再手動新增到我們 HTML 檔案中。實在太太不方便了,所以下一步,來看看怎麼把這個過程也自動化起來吧。
使用 JavaScript 模組打包工具(webpack)
大多數程式語言都會提供從一個檔案匯入另一個檔案程式碼的機制。然而 JavaScript 最初設計時並沒有這個特性,因為 JavaScript 原本是為了在瀏覽器端執行而設計的,並沒有許可權獲取計算機客戶端的檔案系統(安全考慮)。所以很長一段時間以來,組織多個檔案的 JavaScript 程式碼就是把每個檔案下載下來,變數是全域性共享的。
這實際上和我們剛才對 moment.js 的所做的並無二致 — 把整個的 moment.min.js 檔案下載到 HTML 中,定義了一個全域性變數 moment,對於所有在 moment.min.js 之後下載的檔案都可用(無論是否真的需要它)。
2009 年,一個叫做 CommonJS 的專案興起,目標是規範 JavaScript 在瀏覽器之外的生態系統。CommonJS 中很大的一部分便是對模組系統的規範,允許 JavaScript 像大多數程式語言那樣允許匯入匯出程式碼而不用藉助於全域性變數。最知名的對 CommonJS 模組規範的實現便是 node.js。(譯者注:Node 在實現中也並非完全按照規範,而是對模組規範進行了一定取捨,同時增加了一些自身需要的特性。)
如前所述,node.js 是為 JavaScript 設計在伺服器端的一個執行時。假如讓我們用 node.js 模組來重寫前面的例子的話,不需要把整個 monment.min.js 檔案用 HTML 標籤下載下來,你可以直接在 JavaScript 檔案中這樣載入:
// index.jsvar moment = require('moment');console.log("Hello from JavaScript!");console.log(moment().startOf('day').fromNow());
然而,這是在 node.js 中才起作用的模組載入方法, node.js 作為一個伺服器端語言,有許可權訪問計算機的檔案系統,因此工作良好。Node.js 還知道每個 npm 模組的路徑,所以我們不需要寫上 require(‘./node_modules/moment/min/moment.min.js),而可以直接寫 require(‘moment’)— 是不是很貼心~
對 node.js 來說這一切都很棒,但是如果你真的把上面的程式碼執行在瀏覽器中的話,你會得到一個報錯 require is not defined。瀏覽器沒有對檔案系統的許可權,這就意味著用這種方式載入模組很難搞 — 檔案必須被動態地載入,或者同步地載入(減慢執行速度)或者非同步地載入(不能保證時間順序)。
這就是模組打包器隨之出現的原因。JavaScript 模組打包器是一個能在程式碼構建過程(有檔案系統許可權)繞過這個問題並打包生產出兼容於瀏覽器的生產版本(不再需要有檔案系統許可權)的工具。在這個例子中,我們需要一個模組打包器,找到所有 require 語句(它在瀏覽器端的 JavaScript 中是非法的)並把它們替換成想 require 的檔案實際的內容。最終結果是一個打包後的 JavaScript 檔案(沒有 require 語句)!
曾經最流行的模組打包器是 Browserify,於 2011 年釋出,它倡導在前端使用 node.js 風格的 require 語句(這對 npm 成為前端包管理工具的選擇是至關重要的)。2015 年左右,webpack 最終成為更為廣泛使用的包管理器(React 的流行大大推動了這一程序,它充分利用了 webpack 的各種特性)。
來看一下怎麼用 webpack 讓上面的 require(‘moment’) 的例子在瀏覽器中起作用吧。首先,我們需要把 webpack 安裝到專案中。Webpack 本身也是一個 npm 包,所以可以使用命令列愉快地安裝:
$ npm install webpack --save-dev
注意到 —save-dev 引數 — 把它作為開發環境的依賴,而不是生產環境,因為並不需要把它放到伺服器上。你可以在 package.json 檔案中看到對應的更改,已經自動更新如下:
{"name":"modern-javascript-example","version":"1.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC","dependencies":{"moment":"^2.19.1"},"devDependencies":{// 手動加粗"webpack":"^3.7.1"}}
現在 webpack 作為一個 node_modules 中的包已經安裝好了,可以在命令列中像這樣使用 webpack:
$ ./node_modules/.bin/webpack index