JavaScript有著資源豐富的生態圈,但同時也令人在面對這一堆套件、工具以及設定時眼花撩亂、不知所措。TypeScript能用比較嚴謹的方式來開發JavaScript程式,可以大大地提升程式碼的可維護性,也可以增加多人協作時的效率。筆者甚至覺得我們都應該用TypeScript來編寫JavaScript程式會比較好。然而,要建立出一個完整TypeScript專案是一件繁瑣的事情,我們會需要安裝多種套件及工具並撰寫設定檔和程式碼,來使專案能符合我們自己的開發習慣。在這篇文章中,筆者要分享自己建立TypeScript專案的方式。
如果您還不熟悉TypeScript的話,可以先參考《TypeScript學習之路》系列文章。
全域工具
撰寫TypeScript工具除了要安裝typescript
套件來取得tsc
工具外,筆者建議也把套件管理工具改成pnpm
(預設是npm
)。
pnpm
的好處是它的速度很快,且會用連結(link)的方式來存放Node.js套件,可以省下大量的時間和硬碟空間。pnpm
還有一個特性是,在package.json
的dependencies
相關屬性中,必須要把有依賴到(有import
)的套件都明確寫出來,要引入套件B時不能因為dependencies
相關屬性中的套件A已經有依賴套件B了就不把套件B寫在dependencies
的相關屬性中。
搭配depcheck
工具可以檢查dependencies
相關屬性中有無沒用到的套件被寫進來了。
執行以下指令,可以安裝上述的這些全域工具:
IDE 的選用
IDE當然就是用VS Code了,最多人使用也最方便。如果您還沒有用過VS Code,可以參考以下這篇文章:
要用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延伸模組:
code --install-extension dbaeumer.vscode-eslint
code --install-extension ms-azuretools.vscode-docker
code --install-extension ms-vscode-remote.vscode-remote-extensionpack
code --install-extension ritwickdey.LiveServer
接著在VS Code的使用者設定檔(JSON格式)中,加上以下設定:
{
...
"[json]": {
"editor.detectIndentation": false,
"editor.formatOnSave": true,
},
"[jsonc]": {
"editor.detectIndentation": false,
"editor.formatOnSave": false,
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"editor.detectIndentation": false,
"editor.formatOnSave": false,
},
...
}
加入以上設定後,VS Code會在手動儲存(非自動儲存)JSON檔案的時候,如果它是標準的JSON格式,就會進行格式化(自動排版)。同樣地,手動儲存TS檔案的時候,對該檔案執行eslint --fix
指令,自動修正能修正的程式碼語法,並進行程式碼格式化。禁用editor.detectIndentation
可以使VS Code的格式化功能嚴格遵守VS Code設定的editor.tabSize
數值(預設值是4
),才不會不同檔案的TAB字元的空格數量參差不齊。
除了JSON外禁用editor.formatOnSave
是因為JSON是單純的資料傳輸格式,自動在儲存檔案時格式化並不會有什麼影響。但是JSON-C可能有註解排版的問題,所以對其禁用自動格式化。TS檔案則因為ESLint也會進行格式化的關係,所以不需要再多執行一次格式化。
如果是在Windows作業系統上運行VS Code,可以再加入如下的設定來讓TypeScript、JSON和使用Unix系統的換行字元LF(\n
),而不是Windows系統的換行字元CRLF(\r\n
),以減少一些問題。
{
...
"files.eol": "\n",
...
}
建立 TypeScript 專案
不管是什麼類型的TypeScript專案,都可以透過這個小節提供的方式來建立出最基本的雛型。
建立 Node.js 專案
在專案根目錄中執行以下指令來產生出一個Node.js專案必定會有的package.json
檔案:
在這個指令動作的過程中,您可以順便設定package.json
中的部份欄位值,像是版本號碼,專案描述等等。
將package.json
檔案產生出來後,移除其中的main
欄位,再進行如下的修改:
{
...
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=20"
},
...
}
將type
欄位設為module
,可以使這個Node.js專案成為ESM。最大的好處是我們可以直接使用await
,不需要先定義出一個async
的函數再在裡面寫await
。exports
欄位可以設定要用哪個檔案./index.js
作為此ESM預設暴露出來的檔案。engines.node
欄位可以設定這個Node.js專案支援的Node.js版本範圍。
加入 TypeScript
在專案根目錄中執行以下指令來產生出一個TypeScript專案必定會有的tsconfig.json
檔案,此為TypeScript的設定檔。
將tsconfig.json
檔案產生出來後,進行如下的修改:
{
"compilerOptions": {
...
"target": "ES2023",
"module": "Node16",
"moduleResolution": "node16",
"outDir": "./dist",
...
}
}
將compilerOptions.target
的欄位值可以照著Node.js當前的LTS版本所能支援的最新JavaScript版本來設定(Node.js 20就有支援到ES2023),TypeScript預設會用比較舊的版本。
compilerOptions.module
欄位值設為Node16
會依照其它設定來決定編譯出來的模組要不要支援CommonJS,抑或只是純ESM。當package.json
中的type
欄位為module
,且tsconfig.json
中的compilerOptions.moduleResolution
欄位為node16
,TypeScript就會編譯出純ESM。
compilerOptions.outDir
用來設定TypeScript所編譯出來的JavaScript檔案要放置哪個目錄下,其底下的JS檔案路徑會相對於TypeScript編譯器所編譯的所有TS檔案的共同上層目錄(因為沒設定compilerOptions.rootDir
)。筆者習慣把TypeScript原始碼都放在src
目錄下,測試腳本都放在tests
目錄下,編譯好的JavaScript檔案都放在dist
目錄或lib
目錄下。
compilerOptions.moduleResolution
要設為node16
,TypeScript編譯器才可以用Node.js慣用的套件路徑寫法來引用到node_modules
目錄中的套件,且import
的路徑中需要包含副檔名,如此才符合ESM的規範。
接著要建立另外一個專門用來建置專案的TypeScript設定檔,命名為tsconfig.build.json
。檔案內容如下:
{
"extends": "./tsconfig.json",
"include": [
"src/**/*",
]
}
extends
欄位可以用來繼承其它的TypeScript設定檔。此處我們替原先的設定檔添加include
欄位,用來限制TypeScript在建置專案時只去編譯src
目錄下的檔案。
之所以要分兩個TypeScript設定檔,是因為我們希望預設的tsconfig.json
可以直接和ESLint一同使用,讓VS Code可以對整個專案目錄下的JavaScript/TypeScript檔案作型別檢查,而不是只能對src
目錄下的檔案有作用。
然後還要修改package.json
內的scripts
欄位,如下:
{
...
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc -p tsconfig.build.json",
"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
加進專案。所以還要在專案根目錄中執行以下指令,來安裝相關的套件:
接著在專案根目錄中新增eslint.config.mjs
檔案,內容如下:
import eslint from '@eslint/js';
import stylistic from "@stylistic/eslint-plugin";
import * as importPlugin from "eslint-plugin-import";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
ecmaVersion: 2023,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// These rules relate to possible logic errors in code:
"array-callback-return": "error",
"no-constructor-return": "error",
"no-inner-declarations": ["error", "both"],
"no-promise-executor-return": "error",
"no-self-compare": "error",
"no-template-curly-in-string": "warn",
"no-unmodified-loop-condition": "warn",
"no-unreachable-loop": "error",
"require-atomic-updates": "error",
// These rules suggest alternate ways of doing things:
"accessor-pairs": "error",
"arrow-body-style": ["error", "as-needed"],
"block-scoped-var": "error",
"camelcase": ["error", {
properties: "never",
}],
"consistent-this": ["error", "self"],
"curly": "error",
"default-case-last": "error",
"eqeqeq": "error",
"func-names": ["error", "as-needed"],
"func-style": ["error", "expression"],
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"logical-assignment-operators": ["error", "always", {
enforceForIfStatements: true
}],
"new-cap": ["error", {
newIsCap: true,
capIsNew: true,
}],
"no-bitwise": ["error", {
int32Hint: true,
}],
"no-caller": "error",
"no-div-regex": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-multi-assign": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-object-constructor": "error",
"no-octal-escape": "error",
"no-plusplus": ["error", {
allowForLoopAfterthoughts: true,
}],
"no-proto": "error",
"no-return-assign": "error",
"no-sequences": "error",
"no-unneeded-ternary": "error",
"one-var": ["error", "never"],
"operator-assignment": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-exponentiation-operator": "error",
"prefer-numeric-literals": "error",
"prefer-object-has-own": "error",
"prefer-object-spread": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": ["error", "as-needed"],
"require-unicode-regexp": "error",
"sort-imports": ["error", {
ignoreDeclarationSort: true,
}],
"symbol-description": "error",
"vars-on-top": "error",
// These rules care about how the code looks rather than how it executes:
"unicode-bom": "error",
}
},
{
rules: {
// override
"@typescript-eslint/no-require-imports": ["error", {
allowAsImport: true,
}],
"@typescript-eslint/no-unused-vars": ["error", {
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
}],
"@typescript-eslint/restrict-template-expressions": ["error", {
allowBoolean: true,
allowNullish: true,
allowNumber: true,
allowRegExp: true,
}],
// switch on
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": ["error", {
disallowTypeAnnotations: false,
fixStyle: "separate-type-imports",
prefer: "type-imports",
}],
"@typescript-eslint/default-param-last": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {
accessibility: "no-public",
}],
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/method-signature-style": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/no-loop-func": "error",
"@typescript-eslint/no-unnecessary-parameter-property-assignment": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-use-before-define": "error",
"@typescript-eslint/no-useless-empty-export": "error",
},
},
stylistic.configs['recommended-flat'],
{
rules: {
// override
"@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": ["error", "1tbs", {}],
"@stylistic/indent": ["error", 4, {
SwitchCase: 1,
}],
"@stylistic/indent-binary-ops": ["error", 4],
"@stylistic/lines-between-class-members": "off",
"@stylistic/member-delimiter-style": ["error", {}],
"@stylistic/multiline-ternary": ["error", "never"],
"@stylistic/no-extra-parens": ["error", "all", {
nestedBinaryExpressions: false,
}],
"@stylistic/no-mixed-operators": ["error", {}],
"@stylistic/no-multiple-empty-lines": ["error", {
max: 2,
maxEOF: 0,
maxBOF: 0,
}],
"@stylistic/no-trailing-spaces": ["error", {
skipBlankLines: true,
}],
"@stylistic/object-curly-spacing": ["error", "always", {
arraysInObjects: true,
objectsInObjects: true,
}],
"@stylistic/quote-props": ["error", "as-needed"],
"@stylistic/quotes": ["error", "double", {}],
"@stylistic/semi": ["error", "always"],
"@stylistic/semi-spacing": ["error", {
before: false,
after: false,
}],
"@stylistic/spaced-comment": ["error", "always", {}],
"@stylistic/wrap-iife": ["error", "outside", {}],
"@stylistic/yield-star-spacing": ["error", "after"],
// switch on
"@stylistic/array-bracket-newline": ["error", {
minItems: 4,
multiline: true,
}],
"@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/function-call-argument-newline": ["error", "consistent"],
"@stylistic/function-call-spacing": "error",
"@stylistic/function-paren-newline": ["error", "multiline-arguments"],
"@stylistic/generator-star-spacing": ["error", {
before: false,
after: true,
}],
"@stylistic/implicit-arrow-linebreak": "error",
"@stylistic/jsx-quotes": ["error", "prefer-double"],
"@stylistic/linebreak-style": "error",
"@stylistic/newline-per-chained-call": ["error", {
ignoreChainWithDepth: 3,
}],
"@stylistic/no-extra-semi": "error",
"@stylistic/nonblock-statement-body-position": "error",
"@stylistic/object-curly-newline": ["error", {
"ObjectExpression": { "multiline": true, "minProperties": 4 },
"ObjectPattern": { "multiline": true, "minProperties": 4 },
"ImportDeclaration": { "multiline": true, "minProperties": 4 },
"ExportDeclaration": { "multiline": true, "minProperties": 4 }
}],
"@stylistic/object-property-newline": ["error", {
allowAllPropertiesOnSameLine: true,
}],
"@stylistic/semi-style": "error",
"@stylistic/switch-colon-spacing": "error",
"@stylistic/wrap-regex": "error",
}
},
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
{
settings: {
"import/resolver": {
"typescript": true,
"node": true,
}
},
rules: {
// override
// switch on
"import/no-empty-named-blocks": "error",
"import/no-extraneous-dependencies": ["error", {
devDependencies: false,
}],
"import/no-mutable-exports": "error",
"import/no-unused-modules": ["warn", {
unusedExports: true,
src: [
"src/**/*",
],
}],
"import/no-absolute-path": "error",
"import/no-cycle": "error",
"import/no-self-import": "error",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"import/first": "error",
"import/newline-after-import": "error",
"import/no-named-default": "error",
"import/no-namespace": "warn",
"import/order": ["error", {
"newlines-between": "always-and-inside-groups",
alphabetize: {
order: "asc",
},
warnOnUnassignedImports: true,
}],
}
},
{ ignores: ["eslint.config.mjs", "dist/*"], },
);
您也可以自行參考ESLint的官方文件、typescript-eslint
的官方文件、ESLint Stylistic的官方文件和eslint-plugin-import
的官方文件來設定rules
欄位。
另外,由於eslint-plugin-import
有Bug存在,所以還需要新增.eslintrc
檔案到這個專案下,空檔案即可。
設定好ESLint後,使用VS Code來開啟這個TypeScript專案,就可以比較安心地撰寫TypeScript程式碼了!
加入 Git
在專案根目錄中執行以下指令來產生出一個Git專案必定會有的.git
目錄:
接著要在專案根目錄中建立出.gitignore
檔案。考慮到Node.js專案常見的IDE有VS Code和WebStorm這兩個,且考慮到也有一些堅持使用Vim來完成一切文字編輯相關事務的人,筆者的.gitignore
檔案內容如下:
### Intellij+all ### # 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 ### Intellij+all Patch ### # Ignore everything but code style settings and run configurations # that are supposed to be shared within teams. .idea/* !.idea/codeStyles !.idea/runConfigurations ### 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/ # 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
以上設定參考自gitignore.io,異動部份已經標注。
再來要替專案加上Git Hooks。有關於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.json
的scripts
欄位所設定的腳本。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
中。
然後要修改package.json
,如下:
{
...
"exports": "./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 ...
還要編輯eslint.config.mjs
檔案,將ignores
的dist/*
改為lib/*
。
再來是編輯tsconfig.json
檔案,如下:
{
"compilerOptions": {
...
"outDir": "./lib",
"declaration": true,
...
},
...
}
啟用declaration
可以在編譯TypeScript的同時產生出.d.ts
定義檔。
接著修改eslint.config.mjs
檔案的import/no-unused-modules
規則,如下:
{
...
"rules": {
...
"import/no-unused-modules": ["warn", {
unusedExports: true,
src: [
"src/**/*"
],
ignoreExports: [
"src/lib.ts",
],
}],
...
}
}
以上設定,可以讓我們在src/lib.ts
檔案正常使用export
關鍵字。
之後就是建立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專案是要直接使用node
、pm2
等指令來執行的話,就屬於應用程式專案。
首先要執行以下指令將Node.js的定義套件加入devDependancies
中。
再來還要加入nodemon
作為開發階段用來執行專案的工具,它可以在檔案發生變化的時候自動重新執行JavaScript/TypeScript程式。為了使nodemon
能執行TypeScript程式,還需要安裝ts-node
。至於在生產階段執行專案的時候,為了方便偵錯,我們要使用SourceMap來將JavaScript程式對應回TypeScript程式,原生的node
並不支援SourceMap,還需要安裝source-map-support
。所以要執行以下指令來安裝這三個套件:
pnpm i source-map-support
然後要刪除package.json
內的exports
欄位,接著做如下的修改:
{
...
"bin": "./dist/app.js",
...
"scripts": {
...
"start": "NODE_ENV=production node -r source-map-support/register dist/app.js",
"start:dev": "NODE_ENV=development nodemon --exec \"node --loader ts-node/esm src/app.ts\" -e 'ts,json'",
...
}
...
}
start
腳本是正式環境時透過執行npm start
指令來使用,會直接執行JS檔案。start:dev
腳本是在開發環境使用,還可以結合VS Code的偵錯工具。而NODE_ENV
是Node.js程式中常用來判斷執行環境的環境變數。
這裡要注意的是,如果您的應用程式專案有要在Windows環境上運行的話,NODE_ENV
環境變數用以上的方式來設定是會出問題的。解決方法是使用以下指令來安裝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 --exec \"node --loader ts-node/esm src/app.ts\" -e 'ts,json'",
...
}
...
}
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在使用上更為簡易。
執行以下指令來安裝所需的相關套件:
然後要修改package.json
,如下:
{
...
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"test:inspect-brk": "node --experimental-vm-modules --inspect-brk=0.0.0.0:9230 node_modules/jest/bin/jest.js --testTimeout 0 --runInBand",
...
"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環境來偵錯。
prepublishOnly
腳本會在執行npm publish
指令時被自動執行(prepublishOnly
腳本執行完後是prepack
腳本),可以在發佈程式之前檢查是否能通過程式碼校驗和測試。
然後要修改tsconfig.build.json
檔案,排除掉檔名為.spec.ts
或.test.ts
結尾的檔案。這樣的話要寫單元測試也可以在src
目錄中的某個.ts
檔案旁邊建立出一個.spec.ts
或.test.ts
檔案來專門為該.ts
檔案寫單元測試。Jest預設會執行檔名為.spec.ts
或.test.ts
結尾的檔案。
{
...
"exclude": [
"**/*.spec.ts",
"**/*.test.ts"
]
}
要讓Jest能執行TypeScript程式,還要加上jest.config.json
檔案,內容如下:
{
"extensionsToTreatAsEsm": [
".ts"
],
"transform": {
"^.+\\.[jt]sx?$": [
"ts-jest",
{
"useESM": true
}
]
},
"moduleNameMapper": {
"^(\\.\\.?\\/.+)\\.jsx?$": "$1"
}
}
以上的moduleNameMapper
欄位是要將引用到我們自己專案下的js
檔案給去掉副檔名,不然Jest會找不到該檔案。
再來要修改.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
指令。
接著修改eslint.config.mjs
檔案的import/no-extraneous-dependencies
規則,如下:
{
...
"rules": {
...
"import/no-extraneous-dependencies": ["error", {
devDependencies: [
"tests/**/*",
"**/*.test.ts",
"**/*.spec.ts",
],
}],
...
}
}
在撰寫測試的時候,我們可能會需要引用到只有在測試程式中才會使用到的套件,此時應將這類新套件使用pnpm i -D <套件名稱>
指令來安裝,將套件安裝至package.json
檔案的devDependencies
欄位中。如此一來當這個Node.js專案被其它專案引用的時候,就不需要再去相依只有在這個Node.js專案進行測試時才會用到的套件。
eslint.config.mjs
檔案中,import/no-extraneous-dependencies
規則能設定除了指定的路徑外,其餘的地方都不能引用屬於devDependencies
的套件,以免目前這個程式專案被其它專案引用的時候發生缺少套件的問題。而以上設定,可以允許寫在package.json
檔案的devDependencies
欄位中的套件能在我們的測試程式中引用。
最後,我們可以新增tests/template.test.ts
檔案來測試看看Jest框架有沒有加入成功。檔案內容如下:
describe("Hello Jest", () => {
it("should success", () => {
expect(true).toBe(true);
});
});
然後在專案根目錄下執行npm test
指令試試吧!
加入 Web 框架至 TypeScript 專案
不同的Web框架有不同的用法,可以參考下列的延伸文章: