JavaScript有著資源豐富的生態圈,但同時也令人在面對這一堆套件、工具以及設定時眼花撩亂、不知所措。TypeScript能用比較嚴謹的方式來開發JavaScript程式,可以大大地提升程式碼的可維護性,也可以增加多人協作時的效率。筆者甚至覺得我們都應該用TypeScript來編寫JavaScript程式會比較好。然而,要建立出一個完整TypeScript專案是一件繁瑣的事情,我們會需要安裝多種套件及工具並撰寫設定檔和程式碼,來使專案能符合我們自己的開發習慣。在這篇文章中,筆者要分享自己建立TypeScript專案的方式。



如果您還不熟悉TypeScript的話,可以先參考以下這個系列文章:

https://magiclen.org/tag/typescript-學習之路/

全域工具

撰寫TypeScript工具除了要安裝typescript套件來取得tsc工具外,筆者建議也把套件管理工具改成pnpm(預設是npm)。

pnpm的好處是它的速度很快,且會用連結(link)的方式來存放Node.js套件,可以省下大量的時間和硬碟空間。pnpm還有一個特性是,在package.jsondependencies相關屬性中,必須要把有依賴到(有import)的套件都明確寫出來,要引入套件B時不能因為dependencies相關屬性中的套件A已經有依賴套件B了就不把套件B寫在dependencies的相關屬性中。

搭配depcheck工具可以檢查dependencies相關屬性中有無沒用到的套件被寫進來了。

執行以下指令,可以安裝上述的這些全域工具:

npm i -g typescript pnpm depcheck

IDE 的選用

IDE當然就是用VS Code了,最多人使用也最方便。如果您還沒有用過VS Code,可以參考以下這篇文章:

https://magiclen.org/vscode

要用VS Code來開發JavaScript程式,筆者會另外安裝以下的延伸模組:

  • ESLint:ESLint是一個JavaScript Linter,可以用來檢查程式碼語法風格以及格式化程式碼。這個延伸模組可以把ESLint的功能整合進VS Code中,就不用每次都打指令來操作ESLint了。
  • Docker:Docker是一種輕量級的作業系統虛擬化解決方案,可以用來製作開發或是執行JavaScript程式的所需環境。這個延伸模組除了可以讓VS Code支援Dockerfile和Docker Compose的設定檔之外,也有在VS Code內加入管理Docker映像和容器的功能,這樣就不用每次都打指令來操作Docker了。
  • Remote Development:這個延伸模組可以透過SSH開啟遠端的程式專案,也可以開啟Docker容器或是WSL中的程式專案。
  • Live Server:如果您是要開發Web前端相關的專案,這個延伸模組可以快速在本地端啟動一個簡單的HTTP伺服器來幫助您查看您輸出的檔案(例如HTML、CSS、JS)經由HTTP伺服器提供後,在網頁瀏覽器上運行的結果。

接著在VS Code的使用者設定檔(JSON格式)中,加上以下設定:

{
    ...

    "[typescript]": {
        "editor.codeActionsOnSave": {
            "source.fixAll.eslint": true
        },
    }

    ...
}

加入以上設定後,VS Code會在手動儲存(非自動儲存)TS檔案的時候,對該檔案執行eslint --fix指令,自動修正能修正的程式碼語法,並進行程式碼格式化(自動排版)。

如果是在Windows作業系統上運行VS Code,可以再加入如下的設定來讓TypeScript、JSON和使用Unix系統的換行字元LF(\n),而不是Windows系統的換行字元CRLF(\r\n),以減少一些問題。

{
    ...

    "[json]": {
        "files.eol": "\n"
    },
    "[typescript]": {
        "editor.codeActionsOnSave": {
            "source.fixAll.eslint": true
        },
        "files.eol": "\n"
    }

    ...
}

建立 TypeScript 專案

不管是什麼類型的TypeScript專案,都可以透過這個小節提供的方式來建立出最基本的雛型。

建立 Node.js 專案

在專案根目錄中執行以下指令來產生出一個Node.js專案必定會有的package.json檔案:

npm init

在這個指令動作的過程中,您可以順便設定package.json中的部份欄位值,像是版本號碼,專案描述等等。

typescript-start-new-project

加入 TypeScript

在專案根目錄中執行以下指令來產生出一個TypeScript專案必定會有的tsconfig.json檔案:

npx tsc --init

typescript-start-new-project

tsconfig.json檔案產生出來後,進行如下的修改:

{
    "compilerOptions": {
        ...

        "target": "ES2022",
        "outDir": "./dist",
        "rootDir": "./src",
        "moduleResolution": "node",

        ...
    },
    "include": [
        "src/**/*"
    ]
}

compilerOptions.target的欄位值可以照著Node.js當前的LTS版本所能支援的最新JavaScript版本來設定(Node.js 16支援到ES2022),TypeScript預設會用比較舊的版本。

compilerOptions.outDir用來設定TypeScript所編譯出來的JavaScript檔案要放置哪個目錄下,其底下的JS檔案路徑會相對於compilerOptions.rootDir所設定的目錄底下的TS檔案路徑。筆者習慣把TypeScript原始碼都放在src目錄下,測試腳本都放在tests目錄下,編譯好的JavaScript檔案都放在dist目錄或lib目錄下。

compilerOptions.moduleResolution要設為node,TypeScript編譯器才可以用Node.js慣用的套件路徑寫法來引用到node_modules目錄中的套件。

include欄位是設定TypeScript檔案存放的路徑。目前只使用存放於src目錄的TypeScript檔案,未來如果要加上測試,就會再把tests目錄加進來。

然後還要修改package.json內的scripts欄位,如下:

{
    ...

    "scripts": {
        "clean": "rimraf dist",
        "build": "npm run clean && tsc",
        "lint": "eslint src",
        "lint:fix": "npm run lint -- --fix"
    }

    ...
}

如此一來,在專案根目錄下執行npm run clean就可以清除輸出目錄;執行npm run build就可以在清空輸出目錄後,編譯TypeScript程式碼;執行npm run eslint就可以用eslint來檢查src目錄底下的TypeScript程式碼;執行npm run eslint:fix就可以用eslint在檢查程式碼的同時順便進行修正。

當然,現在我們還沒有把tsc、ESLint、rimraf加進專案。所以還要在專案根目錄中執行以下指令,來安裝相關的套件:

pnpm i -D eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin rimraf

接著在專案根目錄中新增.eslintrc檔案,內容如下:

{
    "parser": "@typescript-eslint/parser",
    "plugins": [
        "@typescript-eslint"
    ],
    "env": {
        "es2022": true
    },
    "parserOptions": {
        "project": "./tsconfig.json"
    },
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:@typescript-eslint/recommended-requiring-type-checking"
    ],
    "rules": {
        "@typescript-eslint/no-var-requires": "warn",
        "array-bracket-newline": [
            "error",
            {
                "minItems": 4,
                "multiline": true
            }
        ],
        "array-bracket-spacing": [
            "error",
            "never"
        ],
        "array-element-newline": [
            "error",
            "consistent"
        ],
        "arrow-parens": [
            "error",
            "always"
        ],
        "arrow-spacing": [
            "error",
            {
                "before": true,
                "after": true
            }
        ],
        "block-spacing": [
            "error",
            "always"
        ],
        "brace-style": [
            "error",
            "1tbs"
        ],
        "camelcase": [
            "error",
            {
                "properties": "never"
            }
        ],
        "comma-dangle": [
            "error",
            "always-multiline"
        ],
        "comma-spacing": [
            "error",
            {
                "before": false,
                "after": true
            }
        ],
        "comma-style": [
            "error",
            "last"
        ],
        "computed-property-spacing": [
            "error",
            "never"
        ],
        "consistent-this": [
            "error",
            "self"
        ],
        "dot-location": [
            "error",
            "property"
        ],
        "eol-last": [
            "error",
            "always"
        ],
        "func-call-spacing": [
            "error",
            "never"
        ],
        "func-style": [
            "error",
            "expression"
        ],
        "function-call-argument-newline": [
            "error",
            "consistent"
        ],
        "function-paren-newline": [
            "error",
            "multiline-arguments"
        ],
        "generator-star-spacing": [
            "error",
            {
                "before": false,
                "after": true
            }
        ],
        "implicit-arrow-linebreak": [
            "error",
            "beside"
        ],
        "indent": [
            "error",
            4,
            {
                "SwitchCase": 1
            }
        ],
        "jsx-quotes": [
            "error",
            "prefer-double"
        ],
        "key-spacing": [
            "error",
            {
                "beforeColon": false,
                "afterColon": true
            }
        ],
        "keyword-spacing": [
            "error",
            {
                "before": true,
                "after": true
            }
        ],
        "line-comment-position": [
            "off"
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "lines-around-comment": [
            "off"
        ],
        "lines-between-class-members": [
            "error",
            "always"
        ],
        "max-len": [
            "off"
        ],
        "max-statements-per-line": [
            "error",
            {
                "max": 1
            }
        ],
        "multiline-ternary": [
            "error",
            "never"
        ],
        "new-cap": [
            "error",
            {
                "newIsCap": true,
                "capIsNew": true,
                "properties": true
            }
        ],
        "new-parens": [
            "error",
            "always"
        ],
        "newline-per-chained-call": [
            "error",
            {
                "ignoreChainWithDepth": 3
            }
        ],
        "no-array-constructor": [
            "error"
        ],
        "no-extra-parens": [
            "error"
        ],
        "no-lonely-if": [
            "error"
        ],
        "no-mixed-spaces-and-tabs": [
            "error"
        ],
        "no-mixed-operators": [
            "error"
        ],
        "no-multi-assign": [
            "error"
        ],
        "no-multi-spaces": [
            "error"
        ],
        "no-multiple-empty-lines": [
            "error",
            {
                "max": 2,
                "maxEOF": 0,
                "maxBOF": 0
            }
        ],
        "no-new-object": [
            "error"
        ],
        "no-plusplus": [
            "error",
            {
                "allowForLoopAfterthoughts": true
            }
        ],
        "no-tabs": [
            "error",
            {
                "allowIndentationTabs": true
            }
        ],
        "no-trailing-spaces": [
            "error",
            {
                "skipBlankLines": true
            }
        ],
        "no-unneeded-ternary": [
            "error",
            {
                "defaultAssignment": true
            }
        ],
        "no-whitespace-before-property": [
            "error"
        ],
        "nonblock-statement-body-position": [
            "error",
            "beside"
        ],
        "object-curly-newline": [
            "error",
            {
                "multiline": true,
                "minProperties": 4
            }
        ],
        "object-curly-spacing": [
            "error",
            "always",
            {
                "arraysInObjects": true,
                "objectsInObjects": true
            }
        ],
        "object-property-newline": [
            "error",
            {
                "allowAllPropertiesOnSameLine": true
            }
        ],
        "one-var": [
            "error",
            "never"
        ],
        "operator-linebreak": [
            "error",
            "before"
        ],
        "padded-blocks": [
            "error",
            "never"
        ],
        "padding-line-between-statements": [
            "off"
        ],
        "prefer-exponentiation-operator": [
            "error"
        ],
        "prefer-object-spread": [
            "error"
        ],
        "quote-props": [
            "error",
            "as-needed"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "rest-spread-spacing": [
            "error",
            "never"
        ],
        "semi": [
            "error",
            "always"
        ],
        "semi-spacing": [
            "error",
            {
                "before": false,
                "after": false
            }
        ],
        "semi-style": [
            "error",
            "last"
        ],
        "space-before-blocks": [
            "error",
            "always"
        ],
        "space-before-function-paren": [
            "error",
            {
                "anonymous": "always",
                "named": "never",
                "asyncArrow": "always"
            }
        ],
        "space-in-parens": [
            "error",
            "never"
        ],
        "space-infix-ops": [
            "error",
            {
                "int32Hint": false
            }
        ],
        "space-unary-ops": [
            "error",
            {
                "words": true,
                "nonwords": false
            }
        ],
        "spaced-comment": [
            "error",
            "always"
        ],
        "switch-colon-spacing": [
            "error",
            {
                "after": true,
                "before": false
            }
        ],
        "template-curly-spacing": [
            "error",
            "never"
        ],
        "template-tag-spacing": [
            "error",
            "never"
        ],
        "unicode-bom": [
            "error",
            "never"
        ],
        "wrap-iife": [
            "error",
            "outside"
        ],
        "wrap-regex": [
            "error"
        ],
        "yield-star-spacing": [
            "error",
            "after"
        ]
    }
}

您也可以自行參考ESLint的官方文件來設定rules欄位。

設定好ESLint後,使用VS Code來開啟這個TypeScript專案,就可以比較安心地撰寫TypeScript程式碼了!

加入 Git

在專案根目錄中執行以下指令來產生出一個Git專案必定會有的.git目錄:

git init

接著要在專案根目錄中建立出.gitignore檔案。考慮到Node.js專案常見的IDE有VS Code和WebStorm這兩個,且考慮到也有一些堅持使用Vim來完成一切文字編輯相關事務的人,筆者的.gitignore檔案內容如下:

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

### Node Patch ###
# Serverless Webpack directories
.webpack/

# Optional stylelint cache

# SvelteKit build / generate output
.svelte-kit

### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg  # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]

# Session
Session.vim
Sessionx.vim

# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide

# Support for Project snippet scope
.vscode/*.code-snippets

# Ignore code-workspaces
*.code-workspace

### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml

以上設定來自gitignore.io

再來要替專案加上Git Hooks。有關於Git Hooks的說明可以參考以下這篇文章:

https://magiclen.org/git-hooks

在專案根目錄中新增.githooks目錄,並在該目錄下新增擁有可執行權限的pre-commit檔案,內容如下:

#!/bin/bash

npm run lint

接著修改package.json中的scripts欄位,如下:

{
    ...

    "scripts": {
        ...

        "prepare": "git config core.hooksPath .githooks || exit 0"
    }

    ...
}

prepare腳本會在npm ci或是npm i的時候被執行。當我們clone一個Node.js專案後,通常首先要做的事情就是npm ci或是npm i,所以利用prepare腳本來設定Git Hooks的目錄。

加入 VS Code 設定檔

在專案根目錄中新增.vscode目錄,這個目錄用來存放給VS Code看的設定檔。

.vscode目錄中新增tasks.json檔案,這個檔案可以用來為VS Code添加任務(Task)。內容如下:

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "npm",
            "script": "build",
            "label": "Build",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

tasks欄位底下可以設定多個任務。

每個任務都有type欄位,用來設定這個任務的類型,npm表示要執行package.jsonscripts欄位所設定的腳本。script欄位用來設定要執行的腳本名稱。label欄位用來設定這個任務的名稱。group用來設定這個任務屬於建構(build)還是測試(test)類型的任務,group.isDefault用來設定這個任務是否為group.kind這個類型的預設任務。

以上設定,會將npm run build設為這個Node.js專案在VS Code中時的預設建構任務。在VS Code中,按下Ctrl + Shift + b,就可以執行預設的建構任務,也就是說,會去執行到npm run build

在VS Code的命令選擇區中,可以執行Run Task命令來選取一個任務來執行。

函式庫專案

如果我們現在要建立的TypeScript專案是要給其它專案引入來用的話,就屬於函式庫專案。

除非您很清楚您接下來要實作的函式庫都不會用到Node.js的任何模組,不然首先要執行以下指令將Node.js的定義套件加入dependancies中。

pnpm i @types/node

然後要修改package.json,如下:

{
    ...

    "main": "lib/lib.js",
    "types": "lib/lib.d.ts",
    "files": ["lib"],
    "scripts": {
        "clean": "rimraf lib",
        ...
        "build:watch": "npm run build -- -w",
        ...
        "prepack": "npm run build"
    }

    ...
}

對於函式庫專案,筆者會將編譯出來的JavaScript檔案改放在lib目錄下,這樣在其它Node.js專案引用這個專案中特定JS檔案的時候就會使用foo/lib/bar這樣的路徑,而不是foo/dist/bar,後者看起來怪怪。

以上設定中,types欄位可以用來指定預設的定義檔按路徑,這個檔案等等會說明如何產生;files欄位可以填寫打包(npm pack)這個Node.js專案時要包含進來檔案或是目錄。

build:watch腳本可以讓tsc持續偵測檔案的變化不斷地去進行編譯,方便在撰寫程式碼的過程中即時找出有沒有什麼會編譯錯誤的地方。

prepack腳本會在執行npm pack或是npm publish指令時被自動執行,除了可以在發佈程式之前檢查是否能通過編譯外,還能確保lib目錄下的JavaScript檔案是跟TypeScript原始碼同步的。

再來是編輯.gitignore檔案,加入/lib。如下:

/lib

...

再來是編輯tsconfig.json檔案,如下:

{
    "compilerOptions": {
        ...

        "outDir": "./lib",
        "declaration": true,

        ...
    },

    ...
}

啟用declaration可以在編譯TypeScript的同時產生出.d.ts定義檔。

之後就是建立src/lib.ts檔案,開始製作函式庫啦!

一般來說,函式庫專案輸出的./lib目錄不需要進Git倉庫,所以有在.gitignore檔案中加入/lib來忽略。但如果您並未打算將函式庫專案發佈到npm Registry,而是想要直接透過遠端的Git倉庫來安裝使用的話,./lib目錄就應該要進Git倉庫中。在這樣的情況下,.gitignore檔案中不要忽略/lib,且最好再加上.githooks/pre-push檔案來保證開發端推到遠端Git倉庫上的./lib目錄是和TypeScript原始碼有同步的,該檔案的內容如下:

#!/bin/bash

npm run build
if ! git diff --exit-code lib; then
    git add lib && git commit --no-verify -m "generate dist files"
fi

應用程式專案

如果我們現在要建立的TypeScript專案是要直接使用nodepm2等指令來執行的話,就屬於應用程式專案。

首先要執行以下指令將Node.js的定義套件加入devDependancies中。

pnpm i -D @types/node

再來還要加入nodemon作為開發階段用來執行專案的工具,它可以在檔案發生變化的時候自動重新執行JavaScript/TypeScript程式。為了使nodemon能執行TypeScript程式,還需要安裝ts-node。至於在生產階段執行專案的時候,為了方便偵錯,我們要使用SourceMap來將JavaScript程式對應回TypeScript程式,原生的node並不支援SourceMap,還需要安裝source-map-support。所以要執行以下指令來安裝這三個套件:

pnpm i -D nodemon ts-node
pnpm i source-map-support

然後要修改package.json,如下:

{
    ...

    "main": "dist/app.js",
    "scripts": {
        ...
        "start": "NODE_ENV=production node -r source-map-support/register dist/app.js",
        "start:dev": "NODE_ENV=development nodemon src/app.ts",
        ...
    }

    ...
}

start腳本是正式環境時透過執行npm start指令來使用,會直接執行JS檔案。start:dev腳本是在開發環境使用,還可以結合VS Code的偵錯工具。而NODE_ENV是Node.js程式中常用來判斷執行環境的環境變數。

這裡要注意的是,如果您的應用程式專案有要在Windows環境上運行的話,NODE_ENV環境變數用以上的方式來設定是會出問題的。解決方法是使用以下指令來安裝run-with-node-env套件:

pnpm i run-with-node-env

然後要再次修改package.json,如下:

{
    ...

    "scripts": {
        ...
        "start": "run-with-node-env production node -r source-map-support/register dist/app.js",
        "start:dev": "run-with-node-env development nodemon src/app.ts",
        ...
    }

    ...
}

tsconfig.json檔案要加上如下的設定,使TypeScript編譯器能產生SourceMap檔案,以及讓ts-node能正常動作。

{
    "compilerOptions": {
        ...

        "sourceMap": true,

        ...
    },

    ...

    "ts-node": {
        "files": true
    }
}

再來要在.vscode目錄下新增launch.json檔案,內容如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node-terminal",
            "name": "start:dev",
            "request": "launch",
            "command": "npm run start:dev"
        }
    ]
}

configurations欄位底下可以設定多筆偵錯組態。

每個組態都有type欄位,用來設定這個組態的類型,node-terminal表示要使用VS Code的JavaScript Debug Terminal。name欄位用來設定這個組態的名稱,會顯示在VS Code偵錯工具的組態選單中。request欄位用來設定這個組態是要啟動(launch)新的行程(process),還是要將已有的行程給附加(attach)進來。command用來設定這個組態要執行的指令。

有了這個組態設定,在VS Code的偵錯工具中,就可以選擇start:dev來執行與偵錯。

之後就是建立src/app.ts檔案,開始製作應用程式啦!

加入 Jest 測試框架至 TypeScript 專案

開發JavaScript/TypeScript程式,測試是非常重要的一環。筆者選用的測試工具是Facebook開發的Jest測試框架,比起同樣很多人使用的Mocha,Jest在使用上更為簡易。

執行以下指令來安裝所需的相關套件:

pnpm i -D jest ts-jest @types/jest

然後要修改package.json,如下:

{
    ...

    "scripts": {
        "test": "jest",
        "test:coverage": "jest --coverage",
        "test:inspect-brk": "node --inspect-brk=0.0.0.0:9230 node_modules/jest/bin/jest.js --testTimeout 0 --runInBand",
        ...
        "build": "npm run clean && tsc -p tsconfig.build.json",
        ...
        "lint": "eslint src tests",
        ...
        "prepublishOnly": "npm run lint && npm run test"
    }

    ...
}

test腳本會執行jest進行測試。test:coverage腳本可以匯出測試覆蓋率。test:inspect-brk腳本可以在執行測試的時候先等待測試行程附加至偵錯工具後才開始動作,之所以監聽0.0.0.0而不是127.0.0.1是為了讓測試即便在Docker容器中運行,也還是可以在Host環境來偵錯。

build腳本對tsc指令進行修改,讓它使用另一個TypeScript專案設定檔。lint腳本則多去檢查tests目錄,也就是我們之後會放置測試腳本的目錄。

prepublishOnly腳本會在執行npm publish指令時被自動執行(prepublishOnly腳本執行完後是prepack腳本),可以在發佈程式之前檢查是否能通過程式碼校驗和測試。

再來是編輯tsconfig.json檔案,如下:

{
    "compilerOptions": {
        ...
        // "rootDir": "./src",
        "rootDirs": [
            "./src",
            "./tests"
        ]
        ...
    },
    "include": [
        "src/**/*",
        "tests/**/*"
    ],
    ...
}

如上,要將原先的compilerOptions.rootDir註解掉,並設定rootDirs欄位。include欄位也要把tests目錄加進來。

然後要新增tsconfig.build.json檔案,把tests目錄排除掉,避免測試腳本在建置專案的時候被編譯。tsconfig.build.json檔案的內容如下:

{
    "extends": "./tsconfig.json",
    "exclude": [
        "tests/**/*",
        "**/*.spec.ts",
        "**/*.test.ts"
    ]
}

以上設定,除了排除掉tests目錄之外,也排除掉檔名為.spec.ts.test.ts結尾的檔案。這樣的話要寫單元測試也可以在某個.ts檔案旁邊建立出一個.spec.ts.test.ts檔案來專門為該.ts檔案寫單元測試。Jest預設會執行檔名為.spec.ts.test.ts結尾的檔案。

要讓Jest能執行TypeScript程式,還要加上jest.config.json檔案,內容如下:

{
    "preset": "ts-jest",
    "testEnvironment": "node"
}

再來要修改.vscode/tasks.json檔案,添加新任務進去。如下:

{
    ...

    "tasks": [
        ...

        {
            "type": "npm",
            "script": "test",
            "label": "Test",
            "group": {
                "kind": "test",
                "isDefault": true
            }
        }
    ]
}

以上設定,會將npm run test設為這個Node.js專案在VS Code中時的預設測試任務。在VS Code的命令選擇區中,可以執行Run Test Task命令來執行預設的測試任務,當然,您也可以替這個命令設定快速鍵(設成Alt + t還蠻好用的,而Run Build Task則可以改設為Alt + b)。

再來要修改.vscode/launch.json檔案,添加新組態進去。如果您是函式庫專案,並不會有.vscode/launch.json檔案,請自行新增,相關說明可以參考建立應用程式專案的小節。將.vscode/launch.json檔案修改如下:

{
    "version": "0.2.0",
    "configurations": [
        ...

        {
            "type": "node-terminal",
            "name": "test",
            "request": "launch",
            "command": "npm test"
        },
        {
            "type": "node",
            "name": "test:inspect-brk",
            "request": "attach",
            "port": 9230
        }
    ]
}

執行test:inspect-brk組態之前,要先執行npm run test:inspect-brk指令。

最後,我們可以新增tests/template.test.ts檔案來測試看看Jest框架有沒有加入成功。檔案內容如下:

describe("Hello Jest", () => {
    it("should success", () => {
        expect(true).toBe(true);
    });
});

然後在專案根目錄下執行npm test指令試試吧!

加入 Web 框架至 TypeScript 專案

不同的Web框架有不同的用法,可以參考下列的延伸文章: