[Node.js] 程式設計教學:建立網頁程式樣板專案

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    在現代語言中,專案產生器 (project generator) 是標準的工具之一。藉由標準化的專案配置,程式人可以減少建置專案的時間,專心在程式開發上。

    然而,在 JavaScript 和 Node.js 中,沒有這麼方便的工具。這是因為 JavaScript 原本是內嵌 (embedding) 在網頁中的腳本語言,並不是一個通用型語言。Node.js 問世後,出現許多專案建置的工具,但 JavaScript 程式設計師對於工具的使用尚未達到共識,每項功能都有多個工具可選。這也造成 JavaScript 和 Node.js 龐雜的生態系。

    現階段的實務是建立樣板專案 (boilerplate project),之後要建立新專案時就拉樣板專案來改。網路上有許多現成的樣板專案,但 JavaScript 生態圈的工具繁多,別人的樣板專案不一定符合自己的需求。如果時間允許的話,最好還是從頭打造自己的樣板專案。筆者以為每個開發團隊都應該維護一份自己的樣板專案。

    本文以實際的案例,展示建立樣板專案的過程。為了便於追蹤程式碼,我們把寫好的專案放在這裡。雖然這也是一個可立即使用的樣板專案,讀者應該還是要試著動手做一次,才能夠真正學到東西。

    這篇文章稍微長一些,讀者可搭配著寫好的樣板專案來看,比較容易了解建置專案的過程。

    開啟新專案

    在網頁程式專案中,我們以單一目錄為中心,將程式碼和設定檔集中在該目錄中。之後在操作專案時,都會以該目錄的根目錄為基準點來設置專案。參考指令如下:

    $ mkdir path/to/project
    $ cd path/to/project
    

    傳統的網頁程式是以後端語言為中心,前端程式碼視為靜態資源 (assets),附帶在專案中。新的趨勢會將前後端程式拆分成兩個專案,兩者間藉由 CORS 等方式來溝通。我們假定專案以網頁前端程式為主軸,是否要和後端程式拆分由讀者自行考量。

    專案架構

    在專案中,我們不會把所有的東西都放在根目錄中,而會使用子目錄適當地區隔不同的東西。Node.js 專案沒有限定子目錄的名稱,以下是一些常見的子目錄名稱:

    • build/ :存放 Gulp 設定檔
    • src/ :存放網頁模板
    • assets/ :存放 CSS、JavaScript、圖檔等
    • data/ :存放 JSON 等資料,用於載入網頁模板中
    • public/ :存放輸出的網站
    • node_modules/ :存放第三方 Node.js 套件

    如果專案有上版本控制軟體的話,請將 public/node_modules/ 設到忽略清單中。在這個專案中, public/ 中的內容視為程式的產出 (output),每次編譯專案時會清空後重新產生,故不應直接手動編輯其中的內容。而 node_modules/ 存放的是第三方套件,這些套件可以輕易地從網路上取得,故不應放入版控中,以免占據不必要的空間。

    HTML 模板和其他的資源分開是管理上的風格。其實可以放在同一個資料夾中,只要專案設置檔配置得宜即可。

    (選擇性) 配置 .gitignore

    .gitignoreGit 所用的忽略清單,如果專案有用 Git 管理的話,適當地設置 .gitignore 會使得專案易於管理。不同類型的專案的 .gitignore 配置會相異。github/gitignore 專案提供許多類型的專案可用的 .gitignore 起始設置檔。

    最直覺的使用方式是到該專案手動複製該專案的 .gitignore 範例後貼到自己的 .gitignore 設定檔上,但有聰明的開發者將這個小動作自動化。可下載 joe 這個小工具,該工具以 Golang (Go 語言) 寫成,參考指令如下:

    $ go get github.com/andrewmeissner/joe
    

    我們下載的是修正過的版本。這個修正版修復了原本的 joe 在 Windows 系統上的問題。

    第一次使用時,要先更新 joe 的資料:

    $ joe u
    

    其實 joe 只是到 github/gitignore 下載檔案後把資料存在本地端而已。

    用以下指令可列出 joe 可用的 .gitignore 類型:

    $ joe ls
    

    以本文範例專案來說,我們需要 Node.js 類型的 .gitignore ,故用以下指令生成相對應的 .gitignore 並存到專案中:

    $ joe g node > .gitignore
    

    上述指令會清空原本的 .gitignore 後再寫入新的資料。如果原本 .gitignore 不為空,可將指令修改如下:

    $ joe g node >> .gitignore
    

    這時就會將新的內容加到原本的 .gitignore 的尾端,而不會清掉原本的內容。

    在專案中引入 NPM

    到目前為止,我們都還沒真正引入 Node.js 套件。透過以下指令,可將專案加入 Node.js 相關的設定檔:

    $ npm init -y
    

    其實這個指令原本的用意是將專案初始化成 NPM 套件。但我們沒有要做 NPM 套件,只是藉此引入一些第三方 Node.js 套件所需的設定檔。

    在輸入該指令後,專案會多出 package.jsonpackage-lock.json 兩個設定檔。前者是 Node.js 專案的主要設定檔,後者用來指定第三方 Node.js 套件的版本。若有使用 Git 等版控的話,請將兩者皆加入版控之中。

    使用 Gulp 驅動任務

    我們引入的第一個 Node.js 套件是 Gulp。Gulp 是一種建置自動化 (build automation) 軟體,作用類似於類 Unix 系統上的 Make。由於很多 Node.js 套件是以函式庫的形式發佈,藉由 Gulp,我們就不需要幫每個套件重寫一次指令。

    Gulp 並不是唯一的建置自動化軟體,還有其他常見的方案,像是 GruntwebpackParcel 等。Gulp 算是相對易用的方案,故本範例專案使用 Gulp。

    透過以下指令可在專案中安裝 Gulp:

    $npm install --save-dev gulp
    

    以這種方案安裝 gulp 套件,算是局部安裝 (local installation)。有些教材會建議安裝 gulp-cli,該套件會指供全域使用的 gulp 指令。但 gulp-cli 套件是不必要的,因為我們可用 npm 指令搭配適當的設置在專案中間接呼叫 Gulp。

    Gulp 預設的專案設定檔是 gulpfile.js ,通常會放在專案根目錄中。但本範例專案將設定檔寫到 build/build.js ,這是為了將所有 Gulp 相關的設定檔集中在 build/ 的緣故。日後我們會將 Gulp 設定檔拆分,以免 Gulp 設定檔過長。

    一開始還沒有實際的任務要跑,我們先寫一個空白 (dummy) 設定檔:

    exports.default = function (cb) {
        // Implement it later.
        cb();
    };
    

    並修改 package.json ,加入以下設定:

    "scripts": {
        "dev": "npm run development",
        "development": "gulp --gulpfile=build/build.js"
    },
    

    日後我們輸入 npm run dev 指令時,npm 就會間接呼叫 Gulp。

    加入 Nunjucks 支援

    在本節中,我們要幫樣板專案加入 HTML 模板的功能。使用 HTML 模板可將網頁模組化,也可將資料和版面分離。我們這裡採用 Nunjucks,這套 HTML 模板語言算是功能齊全,又有 Mozilla 基金會加持。

    輸入以下指令以安裝 Nunjucks:

    $ npm install --save-dev gulp-nunjucks-render
    

    我們另外要安裝 del 套件:

    $ npm install --save-dev del
    

    這個套件用於批次刪除檔案。

    我們在 build/tasks/html/build.js 加入相關的任務:

    const gulp = require('gulp');
    const nunjucks = require('gulp-nunjucks-render');
    
    module.exports = function () {
        return gulp.src('../src/**/*.html')
            .pipe(nunjucks({
                path: ['../src']
            }))
            .on('error', function (err) {
                if (err) {
                  console.error('error', err.message)
          
                  process.exit(1)
                }
            })
            .pipe(gulp.dest('../public'));
    };
    

    在這裡,我們把 HTML 模板導給 Nunjucks 處理後,將其輸出到 public/ 子目錄中。

    此外,我們在 build/tasks/html/clean.js 中加入刪除網頁的任務:

    const del = require('del');
    
    module.exports = function () {
        return del('../public/**/*.html', { force: true });
    };
    

    我們得改寫 Gulp 的設定檔 (本專案為 build/build.js),加入編譯網頁模板相關的任務:

    const gulp = require('gulp');
    
    /* HTML Tasks */
    gulp.task('html:build', require('./tasks/html/build'));
    gulp.task('html:clean', require('./tasks/html/clean'));
    
    /* Domain Tasks */
    gulp.task('html', gulp.series('html:clean', 'html:build'));
    
    gulp.task('default', gulp.parallel('html'));
    

    由於我們把編譯網頁的任務加入預設任務項目中,故可直接呼叫。

    加入即時預覽

    在開發專案程式的過程中,若有即時預覽的功能,程式人可以邊寫邊看輸出,無形中開發迭代更快速。藉由結合 BrowserSync 和 Gulp,我們可以在專案中啟用這樣的功能。

    要啟用這個功能,需要安裝以下套件:

    $ npm install --save-dev browser-sync gulp-notify
    

    我們在 build/lib/message.js 撰寫一個輔助用小函式:

    const notify = require('gulp-notify');
    
    module.exports = {
      error: (title) => {
        return notify.onError({
          title: title,
          message: '\n<%= error.message %>'
        })
      }
    };
    

    當程式出現錯誤時,該函式會啟動系統的提醒 (notification),算是一個貼心的小功能。

    我們要修改 Gulp 的設定檔,加入即時預覽的功能:

    const gulp = require('gulp');
    const message = require('./lib/message');
    
    const browserSync = require('browser-sync').create();
    
    // Omit some code.
    
    function reload (done) {
      browserSync.reload();
      done();
    }
    
    gulp.task('watch', function () {
      browserSync.init({
        open: false,
        server: {
          baseDir: '../public',
          index: 'index.html'
        }
      });
    
      gulp.watch('../src/**/*.html', gulp.series('html', reload))
        .on('error', message.error('WATCH: Views'));
    });
    

    一開始,我們以 BrowserSync 在 public/ 子目錄啟動一個本地端的網頁伺服器。再以 Gulp 監看檔案是否有變化。當專案的網頁模板出現變化時,就呼叫 BrowserSync 套件重新載入網頁伺服器。藉由兩個套件間的合作來達到即時預覽的功能。

    我們還要修改 package.json ,加入相對應的指令:

    "scripts": {
        "dev": "npm run development",
        "development": "gulp --gulpfile=build/build.js",
        "watch": "npm run development && gulp watch --gulpfile=build/build.js"
    },
    

    之後輸入 npm run watch 指令時,就可以在 http://localhost:3000/ 觀看專案的輸出。

    加入 Sass 等 CSS 開發工具支援

    現在程式人較少手寫純 CSS,通常會使用一些 CSS 開發工具來簡化 CSS 撰寫的過程。在本範例專案中,我們使用 SassAutoprefixer。以下列指令來安裝:

    $ npm install --save-dev gulp-sass gulp-sass-glob gulp-autoprefixer
    

    Sass 語法是 CSS 的延伸,在 CSS 設定檔中加入程式語言的功能,簡化 CSS 撰寫的過程。而 Autoprefixer 是 PostCSS 眾多外掛中最受歡迎的,會自動在 CSS 中加上瀏覽器提供者 (vendor) 特有的標頭,我們就不用手動做這些事情。

    此外,我們使用 stylelint 來檢查專案的 Sass 語法。用以下指令安裝 stylelint 相關套件:

    $ npm install --save-dev gulp-stylelint stylelint stylelint-config-recommended
    

    使用 stylelint 等代碼檢查軟體不是必需的,但有助於提升代碼的品質。

    build/tasks/sass/build.js 加入編譯 Sass 代碼相關的任務:

    const gulp = require('gulp');
    const sass = require('gulp-sass');
    const sassGlob = require('gulp-sass-glob');
    const prefix = require('gulp-autoprefixer');
    
    const message = require('../../lib/message');
    
    module.exports = function () {
        return gulp.src('../assets/sass/**/*.scss')
            .pipe(sassGlob())
            .pipe(sass())
            .on('error', message.error('SASS: Compilation'))
            .pipe(prefix({
                cascade: false
            }))
            .pipe(gulp.dest('../public/css'));
    };
    

    在這個任務中,我們將 Sass 代碼導給 Sass 套件編譯成原生 CSS 代碼,再導給 Autoprefix 進行後處理,最後將輸出導向 public/css 子目錄。

    在使用 Autoprefixer 時,要設定瀏覽器的版本。在專案根目錄加上 .browserlistrc ,加入以下內容:

    last 4 versions
    

    讀者可隨專案的需求自行調整想對應的瀏覽器版本。

    build/tasks/sass/lint.js 加入代碼檢查 (linting) 相關的任務:

    const gulp = require('gulp');
    const stylelint = require('gulp-stylelint');
    
    const message = require('../../lib/message');
    
    module.exports = function () {
        return gulp.src('../assets/sass/**/*.scss')
            .pipe(stylelint({
                reporter: [{ formatter: 'string', console: true }]
            }))
            .on('error', message.error('SASS: Linting'));
    };
    

    stylelint 需要藉由設置來調整其行為,比較簡單的方式是用套件配置好的設置。在根目錄中加上 .stylelintrc ,加入以下內容:

    {
      "extends": "stylelint-config-recommended"
    }
    

    在 Gulp 主設置檔中加入 Sass 相關的任務:

    /* SASS Tasks */
    gulp.task('sass:build', require('./tasks/sass/build'));
    gulp.task('sass:clean', require('./tasks/sass/clean'));
    gulp.task('sass:lint', require('./tasks/sass/lint'));
    
    /* Domain Tasks */
    gulp.task('sass', gulp.series('sass:clean', 'sass:lint', 'sass:build'));
    

    由於 stylelint 可以解析 Sass 代碼,故我們把代碼檢查的順序排在代碼編譯之前。

    也要修改即時預覽部分的代碼:

    gulp.task('watch', function () {
      browserSync.init({
        open: false,
        server: { baseDir: '../public' }
      });
    
      gulp.watch('../src/**/*.html', gulp.series('html', reload))
        .on('error', message.error('WATCH: Views'));
    
      gulp.watch('../assets/sass/**/*.scss', gulp.series('sass', reload))
        .on('error', message.error('WATCH: Sass'));
    });
    
    gulp.task('default', gulp.parallel('html', 'sass'));
    

    主要的修改是加入 Sass 代碼監看和同步化的功能。

    支援現代 JavaScript

    在 ES6 之後,JavaScript 的確在語法上獲得一些改善。但目標瀏覽器不一定能即時跟上這些新的特性。在實務上,我們會用 Babel 將 JavaScript 程式碼中的 ES6+ 語法轉為等效的 ES5 語法後,再將網頁程式發佈出去。用以下指令安裝 Babel:

    $ npm install --save-dev gulp-babel @babel/preset-env 
    

    Babel 在使用時,需搭配相對應的外掛。 @babel/preset-env 是官方提供的外掛,也是最常使用的。

    除了用 Babel 轉換 JavaScript 代碼外,還可以用 ESLint 檢查 JavaScript 代碼。用以下指令安裝 ESLint:

    $ npm install --save-dev gulp-eslint babel-eslint
    

    原本 ESLint 目標對象是正規 JavaScript 代碼,我們額外安裝 babel-eslint 套件,讓 ESLint 可檢查 Babel 所能解析的代碼。

    代碼檢查不是必要的項目,但可改善代碼的品質。JavaScript 在工程性上算是偏弱的,故我們用 ESLint 檢查 JavaScript 代碼。

    我們在 build/tasks/javascript/build.js 加入編譯 JavaScript 代碼的任務:

    const gulp = require('gulp');
    const babel = require('gulp-babel');
    
    const message = require('../../lib/message');
    
    module.exports = function () {
        return gulp.src('../assets/js/**/*.js')
            .pipe(babel({
                presets: ['@babel/preset-env']
            }))
            .on('error', message.error('JAVASCRIPT: Building'))
            .pipe(gulp.dest('../public/js'));
    };
    

    另外在 build/tasks/javascript/lint.js 加入代碼檢查 (linting) 相關的任務:

    const gulp = require('gulp');
    const eslint = require('gulp-eslint');
    
    const message = require('../../lib/message');
    
    module.exports = function () {
        return gulp.src('../assets/js/**/*.js')
            .pipe(eslint())
            .pipe(eslint.format())
            .pipe(eslint.failAfterError())
            .on('error', message.error('JavaScript: Linting'));
    };
    

    使用 ESLint 時,需在專案根目錄加上 .eslintrc ,並加入相關的設定:

    {
        "parser": "babel-eslint",
        "rules": {
            "strict": 1
        }
    }
    

    在 Gulp 主設定檔中加入相關任務:

    /* JavaScript Tasks */
    gulp.task('javascript:build', require('./tasks/javascript/build'));
    gulp.task('javascript:clean', require('./tasks/javascript/clean'));
    gulp.task('javascript:lint', require('./tasks/javascript/lint'));
    
    /* Domain Tasks */
    gulp.task('javascript', gulp.series('javascript:clean', 'javascript:lint', 'javascript:build'));
    

    同時也要修改即時預覽的部分:

    gulp.task('watch', function () {
      browserSync.init({
        open: false,
        server: { baseDir: '../public' }
      });
    
      gulp.watch('../src/**/*.html', gulp.series('html', reload))
        .on('error', message.error('WATCH: Views'));
    
      gulp.watch('../assets/sass/**/*.scss', gulp.series('sass', reload))
        .on('error', message.error('WATCH: Sass'));
    
      gulp.watch('../assets/js/**/*.js', gulp.series('javascript', reload))
        .on('error', message.error('WATCH: JavaScript'));
    });
    
    gulp.task('default', gulp.parallel('html', 'sass', 'javascript'));
    

    (選擇性) 以 Flow 檢查 JavaScript 代碼的型別

    JavaScript 是動態型別且弱型別語言,無法對 JavaScript 代碼進行型別檢查。目前的實務是在代碼中額外加上型別標註,輸出原生 JavaScript 時再將這些標註抹除。除了使用 TypeScript 外,也可以用 Flow 搭配 Babel,對 JavaScript 代碼進行型別檢查。

    請輸入以下指令來安裝相關套件:

    $ npm install --save-dev @babel/preset-flow flow-bin
    

    @babel/preset-flow 的用途是在 Babel 代碼中抹除型別標註。實際進行代碼型別檢查的是 flow-bin 套件。

    要在 package.json 中加入新的指令:

    "scripts": {
        "flow": "flow"
    },
    

    第一次在專案中使用 Flow 時請輸入以下指令:

    $ npm run flow init
    

    這時候會在專案根目錄多出 .flowconfig 。設置如下:

    [ignore]
    .*/build/.*
    .*/node_modules/.*
    
    [include]
    .*/assets/js/.*
    
    [libs]
    
    [lints]
    
    [options]
    
    [strict]
    

    這樣設置的目的是忽略不需檢查的 JavaScript 代碼,以節約型別檢查的時間。

    爾後輸入以下指令即可進行型別檢查:

    $ npm run flow
    

    Flow 並不是必要的部分,讀者可視專案需求自行決定是否要在專案中引入 Flow。

    區分開發環境 (Development Environment) 和生產環境 (Production Environment)

    在先前的設置中,我們沒有區分開發環境和生產環境。但不同環境中對代碼的需求會有一些差別。像是在生產環境中,我們會將網頁、CSS 代碼、JavaScript 代碼等縮小 (minification),以節省頻寬。但在開發環境中,我們希望這些代碼保持整齊,以利追蹤和除錯。我們可在 Gulp 設定檔中加入對環境的偵測,並對不同環境採取相異的行為。

    請輸入以下指令以安裝在環境相關的套件:

    $ npm install --save-dev isdev gulp-if cross-env
    

    如果想縮小 (minifying) 網頁,請安裝以下套件:

    $ npm install --save-dev gulp-htmlmin
    

    如果想縮小 CSS 代碼,請安裝以下套件:

    $ npm install --save-dev gulp-clean-css
    

    如果想縮小 JavaScript 代碼,請安裝以下套件:

    $ npm install --save-dev gulp-uglify
    

    我們需要修改多個設定檔,故以 build/tasks/javascript/build.js 為例,看一下更動的地方:

    const isdev = require('isdev');
    
    const gulp = require('gulp');
    const gulpif = require('gulp-if');
    
    const babel = require('gulp-babel');
    const uglify = require('gulp-uglify');
    
    const message = require('../../lib/message');
    
    module.exports = function () {
        return gulp.src('../assets/js/**/*.js')
            .pipe(babel({
                presets: ['@babel/preset-flow', '@babel/preset-env']
            }))
            .on('error', message.error('JavaScript: Building'))
            .pipe(gulpif(!isdev, uglify()))
            .on('error', message.error('JavaScript: Minification'))
            .pipe(gulp.dest('../public/js'));
    };
    

    和先前的差別在於引入 .pipe(gulpif(!isdev, uglify())) 這段流程。該流程在目前環境不為開發環境時,將 JavaScript 代碼縮小。

    在本範例專案中,我們對其他類型的代碼和靜態資源也有相對應的設置,請讀者自行追蹤專案程式碼。

    繼續深入

    到目前為止,我們對於網頁前端的三大核心技術,都設置好相對應的工具,這個樣板專案應該是夠用了。不過,我們可以視自行的需求,再擴展該專案,以符合自身的需求。

    我們後來對圖檔 (image) 和字體 (font) 等網頁中常用的靜態資源加入了相關的編譯指令;像是在生產環境中,將圖檔縮小,以節約頻寬。讀者可追蹤一下範例專案中相對應的部分。

    我們將外部的 JSON 資料和網頁模板進行連動。之後我們可以把資料寫在 JSON 中,而不用寫死在縮頁模板內。藉由這項設置,我們將版面和資料拆分,更有利於縮護。

    我們在專案中引入 Bootstrap,但卻刻意不引入 jQuery,而用 Bootstrap.Native 取代。因為現代瀏覽器在標準化上做得比較好,jQuery 變得不是那麼重要。此外,由於前端框架的興起,繼續使用 jQuery 顯得多餘。

    讀者可以動動腦,想一下自己還需要什麼,試著再擴充自己的樣板專案。

    結語

    打造自己的樣板專案,看似麻煩,不若直接拉別人寫好的樣板專案來得快。但 Node.js 生態圈如此龐大,每個開發團隊慣用的套件各有不同。如果每次拉完別人的樣板專案後還得大幅度修改,這時候就應該再刻一個自己的樣板專案。

    現在 GitHub 等專案管理網站中分支 (forking) 他人的樣板專案相當容易,不一定所有的功能都要從頭刻起。可以先試著觀察他人寫好的樣板專案,若不符合自己的習慣,再手刻一個新的也不遲。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk