iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 2
2
Modern Web

TypeScript + React + 雜七雜八系列 第 2

【Day 02】從一個空白資料夾開始,建置單純編譯 .ts 的 webpack 環境

今天要蓋一個基於 webpack 來編譯 TypeScript 的專案,要拆兩種模式來進行開發及編譯

  1. dev mode,執行 webpack-dev-server 並且可以用 Chrome DevTools 來 debugger
  2. prod mode,執行 webpack 將所有檔案打包成最終的 production 版放進 dist 目錄裡

首先要先找個喜歡的地方開個空白資料夾,筆者這邊開的資料夾名稱是 typescript-react,命名依照個人喜好即可。

接下來安裝全域的 typesript

npm install -g typescript

裝在全域的目的僅僅是為了使用 tsc --init 來產生 tsconfig.json,可以得到完整的定義參數。

進入空白資料夾後,在該目錄內輸入指令

tsc --init
+ tsconfig.json

就會得到一個很多設置屬性都被註解掉的 tsconfig.json 檔,在右半部的文字敘述都有說明屬性設置的功能或是可選的參數值

要注意到的是 tsconfig.json 並沒有包含所有的 Compiler Options
可以從 TypeScript 官方網站提供的表格來看
https://www.typescriptlang.org/docs/handbook/compiler-options.html
表格提供的屬性才是最完整的,不過筆者認為直接看 tsc --init 產生出來的檔案能更快掌握各種屬性說明(因為有分類看起來比較好看)

關於設置檔這邊就不細細說明各個屬性,並直接附上自己覺得配置的比較有趣的設定檔,可以直接複製取用

/* tsconfig.json */
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "ESNext",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    "allowJs": true,                          /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist",                       /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitAny": true,                    /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                 /* Enable strict null checks. */
    "strictFunctionTypes": true,              /* Enable strict checking of function types. */
    "strictBindCallApply": true,              /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    "strictPropertyInitialization": true,     /* Enable strict checking of property initialization in classes. */
    "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true,       /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                          /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  }
}

會說是有趣的部分是它預設時並不會將某些能使 TypeScript 程式碼檢查更嚴格的設置打開,筆者會將部分給打開來,既然用了 TypeScript 就讓它更嚴格的審核你的程式碼吧!

主要是這一些 Strict Type-Checking Options 的屬性

到時候撰寫程式碼時如果沒有遵照這些更嚴格的型別檢查就會通通編譯不過(喂~這應該不有趣阿),這樣可以使你必須更了解自己所寫下的程式碼。


接下來就將需要從 npm 上抓一堆東西來裝一裝

npm init -y
npm i -D clean-webpack-plugin html-webpack-plugin
npm i -D ts-loader typescript
npm i -D webpack webpack-cli webpack-dev-server
npm i -D webpack-dev-server webpack-merge
函式庫名稱 用途 文檔連結
clean-webpack-plugin 每次構建之前清理 dist 資料夾 Link
html-webpack-plugin 創建 HTML 檔,會自動讀取 webpack 打包的檔案,亦可使用 template 決定被創建的 HTML 樣版 Link
ts-loader webpack 處理 .ts 或 .tsx 的檔案 Link
typescript A typed superset of JavaScript Link
webpack 編譯 JavaScript 模組 Link
webpack-cli Command Line Interface,搭配 webpack 一定要裝,實際上好像沒有在單獨拿來使用的指令操作集!? Link
webpack-dev-server webpack 模組開發專用伺服器,會將 bundle 的檔案保存在內存中 Link
webpack-merge Merge designed for Webpack Link

安裝完畢後就在工作區底下新增 webpack 的設置檔及其他所需檔案

+ config/webpack.common.js
+ config/webpack.dev.js
+ config/webpack.prod.js
+ src/
+ index.html

目前的目錄結構


筆者將 webpack 的設置拆分成三個檔案

  • common 為共用的設置
  • dev 為 webpack-dev-server 專用的設置
  • prod 為打包成最終產品時用的設置

只要利用 webpack-merge 將它們組起來,再搭配

webpack-dev-server --config ./config/webpack.dev.js

webpack --config ./config/webpack.prod.js

這兩種指令即可,將指令放進 package.json

{
  "name": "typescript-react",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --progress --color --config ./config/webpack.dev.js",
    "build": "webpack --config config/webpack.prod.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^3.2.0",
    "ts-loader": "^6.1.0",
    "typescript": "^3.6.3",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1",
    "webpack-merge": "^4.2.2"
  }
}

這樣在工作區目錄只要輸入 npm run dev 或是 npm run build,就能區分要 run 開發用的伺服器,或是打包成 prod 了,後續要配設置也能很乾淨將他們分開來添加至 dev.js、prod.js,或是配在共用的 common.js 裡。

/* config/webpack.common.js */

const path = require('path');

module.exports = {
  entry: path.join(__dirname, '../src/index.ts'),
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, '../dist'),
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.ts(x?)$/,
        use: [
          {
            loader: 'ts-loader',
          },
        ],
      },
    ],
  },
};

webpack.common.js 的內容裡要注意的是目前的 entry 是 src 目錄裡的 index.ts,待會就會新增這個檔案

/* config/webpack.dev.js */

const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    hot: true,
    inline: true,
    open: true,
    overlay: {
      warnings: true,
      errors: true,
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    }),
  ],
});
/* config/webpack.prod.js */

const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html'
    }),
  ],
});

還有 index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>typescript-react</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

HtmlWebpackPlugin 的 template 會依照這個 HTML 內容
主要是有多這一段,待會寫 ts 的時候就會用到

<div id="app"></div>

終於要開始寫第一個 .ts 了!
我們在 src 目錄底下新增一個 index.ts

+ src/index.ts

然後輸入程式碼

const app = document.querySelector('app');
app.textContent = 'hello typescript';

然後 npm run dev

就會顯示這個畫面

晴天霹靂,是什麼東西居然會是 null!

從編輯器上的程式碼可以看到一些端倪

app 顯示了紅色底線

原來是 app 可能是 Element 又可能是 null 的關係阿

再往回追,原來是 querySelector 的回傳值型別確實就是 Element | null

那就只能將 null 的情況給排除了

const app = document.querySelector('app');
if (app) {
  app.textContent = 'hello typescript';
}

沒想到我還真給他打錯了 selector 語法,取 id 沒有加 # 字號

不過剛好就先示範用 Chrome 的 DevTools 來 debug

直接按下 F12,面板 tabs 選擇至 Sources

左側目錄集選擇 . 裡面就是你開發環境的檔案

或是用搜尋的方式

打開 src/index.ts 直接下中斷點,重整後就能夠完美中斷

阿~,沒加 # 字號,不過也剛好能示範這段 debug 過程,再次修正它

const app = document.querySelector('#app');
if (app) {
  app.textContent = 'hello typescript';
}

顯示了預期的結果了(字有點小,不要在意)

相信各位在編輯器上也有看到了剛剛那個 querySelector 的回傳值型別,這就是 TypeScript 的厲害之處了,後續還會有更多被 TypeScript 型別檢查到不要不要的情境(M屬性?),這樣就可以更深刻的理解你在寫得程式碼阿!

這下已經有一個可以編譯 TypeScript 又能方便 debuge 的環境啦!

附上原始碼:https://github.com/littlehorseboy/typescript-react/tree/day2-setup-ts-loader


明天就要開始介紹 TypeScript 的各種基本型別(會只介紹有我常用到的)


上一篇
【Day 01】先一個前言
下一篇
【Day 03】TypeScript 基本型別介紹
系列文
TypeScript + React + 雜七雜八30

尚未有邦友留言

立即登入留言