TypeScript-ReactAndWebpack

參考來源

React & Webpack

React & Webpack

這一個篇章會帶你使用 TypeScript 使用 webpack 開發 React

如果你還不知道如何初始化一個新的 React 可以參考這篇文章

換句話說,我們假設你已經會使用 nodejs 和 npm

Lay out the project

開始建立一個新的資料夾 proj

1
mkdir proj && cd proj

然後我們建立一個新的資料結構

1
2
3
4
proj/
├─ dist/
└─ src/
└─ components/

TypeScript 放置於 src 的資料夾中,經過 TypeScript compiler 後再經由 Webpack 最後在 dist 產生一個 bundle.js 的檔案,每一個 components 都會放在 src/components 的資料夾內

初始化專案

1
2
3
$ yarn init
$ yarn add react react-dom
$ yarn add @types/react @types/react-dom -D

types/ 這類的套件代表我們需要他取得 TypeScript 的宣告,通常當你 import 一個套件路徑 react,才找得到 react 的套件,然而並不是所有套件都需要這種宣告套件
,然而並不是所有套件都需要這種宣告套件

安裝開發用套件

1
$ yarn add typescript awesome-typescript-loader source-map-loader -D

這兩個套件一起幫你編譯你的程式碼, awesome-typescript-loader 會依據 tsconfig.json 這個檔案所描述的 TypeScript 標準來做編譯。

source-map-loader 可以在你開發的時候可以做編譯前與編譯後的 mapping 方便追蹤錯誤的程式碼

增加 TypeScript 設定檔案

若你希望你的 TypeScript 整合在一起,你需要一個 tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"
},
"include": ["./src/**/*"]
}

若是你希望學習更多的 tsconfig.json 可以參考這篇文章

範例程式

src/components 建立一個新的 Hello.tsx

1
2
3
4
5
6
7
8
9
10
11
12
import * as React from "react";

export interface HelloProps {
compiler: string;
framework: string;
}

export const Hello = (props: HelloProps) => (
<h1>
Hello from {props.compiler} and {props.framework}!
</h1>
);

然後在 src 中新增一個 index.tsx

1
2
3
4
5
6
7
8
9
import * as React from "react";
import * as ReactDOM from "react-dom";

import { Hello } from "./components/Hello";

ReactDOM.render(
<Hello compiler="TypeScript" framework="React" />,
document.getElementById("example")
);

index.tsx 中只是引入了 Hello.tsx 然後將 Hello component 顯示在頁面上

為了顯示這個 component 我們需要建立一個 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
</head>
<body>
<div id="example"></div>

<!-- Dependencies -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>

<!-- Main -->
<script src="./dist/bundle.js"></script>
</body>
</html>

建立一個 webpack 設定檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "bundle.js",
path: __dirname + "/dist"
},
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
},
module: {
rules: [
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
externals: {
react: "React",
"react-dom": "ReactDOM"
}
};

你可能會好奇 externals 這個欄位在做什麼的?

我們希望在打包的時候希望可以共用一些 package 就好像 global variable 就像 jQuery 或是 _ 一樣

這叫做 namespace pattern, webpack 允許我們使用這個方式來引用套件。

1
$ webpack

Typescript-Migrating from Javascript

翻譯來源

Migrating from JavaScript

從 Javascript 搬移你的程式到 TypeScript

TypeScript 不會憑空存在, 他還是依存於 Javascript 的生態圈內,有很多舊的 Javascript 要轉譯為 TypeScript 過程中很無趣的。

如果你是要轉譯 React project 我們會推薦你先閱讀這份文件

設定你的資料結構

基本的檔案架構會如下

1
2
3
4
5
6
projectRoot
├── src
│ ├── file1.js
│ └── file2.js
├── built
└── tsconfig.json

如果你有想要用測試的話,在 src 再加上 tests 並且在 tsconfig.json 除了 src 之外再加上 tests

寫一個設定檔案

TypeScript 使用 tsconfig.json 來做專案的設定

1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5"
},
"include": [
"./src/**/*"
]
}

我們利用這個設定檔案 對 TypeScript 做一些設定

  1. 包含 include 讀取 src 中所有檔案
  2. 允許 Javascript 文件直接輸入
  3. 可以在 build 輸出所有編譯後文件
  4. 將 ES6或是ES7 轉譯為 ES5

如果你是使用 tsc 轉譯你的專案,你應該會在 built 資料夾裡面看到編譯成功的檔案

優勢

如果你使用的是VS code 或是 Visual Studio 你可以使用相當多的工具,例如自動完成。也可以增加一些設定方便你 debug

  • noImplicitReturns 可以防止你在 function 的最後忘記回傳值
  • noFallthroughCasesInSwitch 可以協助你補上 breakswitch 中的 case

TypeScript 依舊會對無法訪問的標籤的錯誤顯示,你可以利用 allowUnreachableCodeallowUnusedLabels 來取消

整合你的編譯工具

每個人都有自己的編譯步驟

下個範例是我們覺得目前最佳的方式

Gulp

相關的 gulp 使用可以參考我們另外一個文件

Webpack

Webpack 是一個相當簡單的工具!

你可以使用 awesome-typescript-loader 這是一個 TypeScript Loader

另外也可以使用 source-map-loader 讓你更易於 debug

1
$ yarn add awesome-typescript-loader source-map-loader

將上述兩個套件加入你的 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
entry: './src/index.ts'
output: {
filename: './dist/bundle.js'
},

devtool: 'source-map',

resolve: {
extensions: ['', 'webpack.js', '.web.js', '.ts', '.tsx', 'js']
},

moudle: {
loaders: [
{test: /\.tsx?$/, loader: 'awesome-typescript-loader'}
],

preLoaders: [
{test: /\.js$/, loader: 'source-map-loader'}
]
}
}

awesome-typescript-load 必須在所有的 loader 的前面 ts-loader 也是一樣的道理,你可以在這邊獲得更多資訊

將 .js 轉換成為 .ts

我們可以開始對檔案做一些動作開始轉換

第一個步驟就是將所有的 .js 換成 .ts

若是你的檔案有使用 JSX 則需要將檔案名稱換成 .tsx

當然你會覺得怪怪的,這樣就結束了麻?

當然不是!

接著你可以打開你的編輯器或是使用 command line

1
$ tsc --pretty

你可以看到一些 紅色 的波浪底線

這些就像是 微軟的 word 軟體提醒你這些並不符合 TypeScript 的規範

如果這些對你說太寬鬆,你希望可以更加嚴謹的話你也可以使用 noEmitOnError 這一個選項來讓檢查更加嚴謹

如果你希望使用嚴謹模式可以參考這篇文章

例如你不希望變數型態使用不明確的 any 你可以在編輯檔案之前使用 noImplicitAny

解決錯誤相關問題

就像剛剛提到的,你會修改 .js.ts 或是 .tsx 的時候會有相當多的錯誤訊息

你會發現這些錯誤雖然是屬於合法的錯誤,但是透過這些錯誤可以發現 TypeScript 對你開發程式碼的好處。

import 模組

你可能會得到錯誤訊息是 Cannot find name 'require' 或是 Cannot find name 'deffined'

在這些狀況在這些狀況應該是 TypeScript 找不到這些模組

你需要預先選擇使用引入模組的方式,可以使用 commonjs, amd, systemumd

如果你有使用 Nod/CommonJS

1
2
var foo = require('foo');
foo.doStuff();

或是 RequireJS/AMD

1
2
3
define(["foo"], function(foo){
foo.doStuff();
});

你可以修改為

1
2
import foo = require('foo');
foo.doStuff();
TypeScript 的宣告

如果你編譯檔案的時候有安裝 foo module

但是依然看到 Cannot find module 'foo'

這個錯誤訊息,很有可能是你並未有宣告的檔案來宣告你的 library,

要處理這個問題也很簡單

1
$ yarn add @types/lodash -D

如果你有使用 module 選項是 commonjs 你另外需要設定 moduleResolutionnode

然後你才可以正常沒有錯誤訊息的引用 lodash

Export 模組

通常在 export 模組的時候都是使用 export 或是 module.exports

TypeScript 允許你使用 export 參數,例如:

1
2
3
module.exports.feedPets = function(pets){
//....
}

你可以改為

1
2
3
export function feedPets(pets){
//...
}

有時候你會直接複寫 exports object

這是屬於 commonJS 的設計

在引用這類的模組的時候

1
2
var express = require('express');
var app = express();

你在之前會有像這樣的範例程式

1
2
3
4
function foo(){
//...
}
module.exports = foo;

TypeScript 你可以修改為 export = consturct

1
2
3
4
function foo(){
//...
}
export = foo;

有時候你呼叫函式的時候會有太多或是太少的參數,

通常這是一種 bug

但是在有些狀況中你可以宣告函示使用 arguments object 來代替寫出的所有參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function myCoolFunction() {
if (arguments.length == 2 && Array.isArray(arguments[1])) {
var f = arguments[0];
var arr = arguments[1];

arr.map(value => f(value));
} else {
var f = arguments[0];
var arr = arguments[1];
console.log("arguments: ", arguments);
}
}

myCoolFunction(
function(x) {
console.log(x);
},
[1, 2, 3, 4]
);
myCoolFunction(
function(x) {
console.log(x);
},
1,
2,
3,
4
);

result:

1
2
3
4
5
1
2
3
4
arguments: { '0': [Function], '1': 1, '2': 2, '3': 3, '4': 4 }

在這個範例中因為有兩種 input 方式

所以在 TypeScript 需要改為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function myCoolFunction(f: (x: number) => void, nums: number[]): void;
function myCoolFunction(f: (x: number) => void, ...nums: number[]): void

function myCoolFunction() {
if (arguments.length == 2 && Array.isArray(arguments[1])) {
var f = arguments[0];
var arr = arguments[1];

arr.map(value => f(value));
} else {
var f = arguments[0];
var arr = arguments[1];
console.log("arguments: ", arguments);
}
}

myCoolFunction(
function(x) {
console.log(x);
},
[1, 2, 3, 4]
);
myCoolFunction(
function(x) {
console.log(x);
},
1,
2,
3,
4
);
1
$ tsc src/demo1.ts && node src/demo1.js

result

1
2
3
4
5
1
2
3
4
arguments: { '0': [Function], '1': 1, '2': 2, '3': 3, '4': 4 }

TypeScript 中宣告了 myCoolFunction 兩個不同的宣告,

定義一個 f 為 function : (x: number) => void

然後也對多個變數使用 ...nums: number[] 來宣告是一個數字陣列

增加參數

有些人對一個物件增加參數使用下述方式

1
2
3
let options = {};
options.color = 'red';
options.volume = 11;

TypeScript 則會表示這樣無法新增 color 和 volume 因為 property 不存在

1
2
3
4
5
6
7
interface Options {color: string, volume: number};

var options = {}; // Error
var options = {} as Options; // Success

options.color = 'red';
options.volume = 11;

你可以將 type 定義為 object 或是 {} 這樣也可以自由地增加物件特性,但是比較常使用的是 any 來宣告這類型自由的型態

例如你有一個物件類型宣告為 object 你將無法呼叫 toLowerCase() 這個函式,但是若是你宣告的是 any 的類型的話,就可以呼叫這個函式。也就是當你宣告為 any 這個類型的話就不會有太多的錯誤提示你的型別問題

嚴謹模式

TypeScript 可以幫你檢查並且分析你的程式,讓你的錯誤減少

不推薦使用 any

使用了 any 這個類型會造成遇到錯誤時 TypeScript 也不會提醒你,也就喪失使用 TypeScript 的意義,你可以設定 noImplicitAny 這個選項來控制禁止使用 any

更嚴謹的檢查 undefined 和 null

TypeScriptundefinednull 的類型宣告都是 any,但是 undefinednullJavascriptTypeScript 造成許多的 Bug,所以你可以設定 strictNullChecks 來做更嚴謹的檢查。

當你設定 strictNullChecks 的時候 undefinednull 也會擁有他們自己的類型 undefinednull , 若是有些變數同時可能是數字或是 null 的時候,可以利用 union 來宣告類型, 例如

1
2
3
4
declare var foo: string[] | null;

foo.length; // error - 'foo' 有可能是null
foo!.length; // okay - foo! 只可能是 'string[]'

不可宣告 this 為 any

當你使用 this 這個keyword 的時候,預設是 any,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point{
constructor(public x, public y){}
getDistance(p: Point){
let dx = p.x - this.x;
let dy = p.y - this.y;
return Math.sqrt(dx ** 2 + dy ** 2);
}
}

interface Point{
distanceFromOrigin(point: Point): number;
}

Point.prototype.distanceFromOrigin = function(point: Point){
return this.getDistance({x: 0, y: 0});
}

這部分就像我們上面談的問題其實是一樣的

我們沒辦法去檢查 this 這個變數的型態

TypeScript 也有一個 noImplicitThis 的選項

當這個選項被設定為 true 的時候, TypeScript 會提醒你在使用 this 時要設定型態

1
2
3
Poing.prototype.distanceFromOrigin = function(this: Point, point: Point){
return this.getDistance({x: 0, y: 0});
}

TypeScriptWithGulp

翻譯來源

TypeScriptWithGulp

Gulp

此篇是使用 TypeScriptGulp 並且利用 Gulp 的 pipeline增加 Browserify , uglifywatchify 以及 Babelify等等功能

Minimal project

在此範例中建立一個資料夾 proj 但是你可以建立一個你希望的名字的資料夾

1
2
$ mkdir proj
$ cd proj

先簡單建立資料結構

1
2
3
proj/
├─ src/
└─ dist/

初始化

1
2
$ mkdir src dist
$ yarn init

安裝相關的套件

1
2
$ npm install -g gulp-cli
$ yarn add typescript gulp gulp-typescript -D

Example

src 中建立一個新的 main.ts

1
2
3
4
function hello(compiler: string) {
console.log(`Hello from ${compiler}`);
}
hello("TypeScript");

proj 中建立一個 tsconfig.json

1
2
3
4
5
6
7
8
9
{
"files": [
"src/main.ts"
],
"compilerOptions": {
"noImplicitAny": true,
"target": "es5"
}
}

建立一個 gulpfile.js

1
2
3
4
5
6
7
8
9
var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject("tsconfig.json");

gulp.task("default", function () {
return tsProject.src()
.pipe(tsProject())
.js.pipe(gulp.dest("dist"));
});

測試 App

1
2
3
$ gulp
$ node dist/main.js
//result: Hello from TypeScript

增加模組

建立一個新的檔案 src/greet.ts

1
2
3
export function sayHello(name: string){
return `Hello from ${name}`;
}

然後再修改 src/main.ts

1
2
3
import { sayHello } from "./greet";

console.log(sayHello("TypeScript"));

最後再修改 tsconfig.jsonsrc/greet.ts 加入編譯

1
2
3
4
5
6
7
8
9
10
{
"files": [
"src/main.ts",
"src/greet.ts"
],
"compilerOptions": {
"noImplicitAny": true,
"target": "es5"
}
}

執行

1
$ gulp && node dist/main.js

然後就可以看到編譯後執行的結果

1
Hello from TypeScript

Note: 雖然我們使用 ES2015 但是 TypeScript 使用 commonJS 模組,但是你也可以在 options 中設定 module 來改變它

Browserify

我們開始寫前端的程式

我們希望可以把所有 modules 打包到一個 Javascript 檔案

而這些事情就是 browserify 所做的事情,而這會使用到 CommonJS 模組,而這也正好是 TypeScript 預設使用的,也就是我們可以在 TypeScript 直接使用 browserify

先安裝 browserify, tsify, vinyl-source-stream

tsify 是 browserify 的 plugin 做的事情就像是 gulp-typescript 一樣,而vinyl-source-stream 則是提供一種方便我們了解的檔案輸出格式

1
$ yarn add browserify tsify vinyl-source-stream -D

src 中建立一個 index.html 的檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>

<body>
<p id="greeting">Loading ...</p>
<script src="bundle.js"></script>
</body>

</html>

然後修改 main.ts

1
2
3
4
5
6
7
8
import { sayHello } from "./greet";

function showHello(divName: string, name: string) {
const elt = document.getElementById(divName);
elt.innerText = sayHello(name);
}

showHello("greeting", "TypeScript");

接著再修改 gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const gulp = require('gulp');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const tsify = require('tsify')

const paths = {
pages: ['src/*.html']
};

gulp.task('copy-html', () => {
return gulp.src(paths.pages)
.pipe(gulp.dest('dist'));
});

gulp.task('default', ['copy-html'], () => {
return browserify({
basedir: '.',
debug: true,
entries: ['src/main.ts'],
cache: {},
packageCache: {}
})
.plugin(tsify)
.bundle()
.pipe(source('bundle.js'))
.pipe(gulp.dest('dist'));
});

增加一個 copy-html 的task 並且將其加入在 default 的 task中,也就是代表當 default執行的時候會先執行 copy-html 並且也修改 default 的 function 加入呼叫 Browserifytsify 的 plugin,將 tsify 取代 gulp-typescript 也丟入一些參數在 Browserify 之中, 在 bundle 之後 再利用 vinyl-source-stream 輸出檔案 bundle.js

然後我們可以執行之後再用瀏覽器開啟 dist/index.html來觀看結果

Note: 設定 debug: true 是因為 在打包成為一個檔案之後, SourceMap 可以對照你打包後的檔案,當你發生錯誤的時候,就可以找到相關錯誤位置,提高 debug 的效率

watchify, Babel and Uglify

現在我們已經有 tsify, browserify 我們還可以再加入一些套件

  • Watcherify 利用 gulp 啟動,可以保證程式持續執行,並且在修改後同步修改重啟,你的瀏覽器也可以立即 refresh 觀看結果

  • Babel 是一個大型並且彈性的編譯 Lib 可以將 ES2015 轉回 ES5 和 ES3,可以自行增加擴充編譯套件,而這些是 typescript 沒有支援的

  • Uglify 則是將你的程式最小化,讓你下載的時間可以大大減少下載的時間

Watcher

安裝 watchify和 gulp-util

1
$ yarn add watchify gulp-util -D

再修改 gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const gulp = require("gulp");
const browserify = require("browserify");
const source = require("vinyl-source-stream");
const tsify = require("tsify");
const watchify = require('watchify');
const gutil = require('gulp-util');

const watchedBrowserify = watchify(browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {}
})).plugin(tsify);

const paths = {
pages: ["src/*.html"]
};

gulp.task("copy-html", () => {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

function bundle(){
return watchedBrowserify
.bundle()
.pipe(source("bundle.js"))
.pipe(gulp.dest("dist"));
}
gulp.task("default", ["copy-html"],bundle);

watchedBrowserify.on('update', bundle);
watchedBrowserify.on('log', gutil.log);

gulpfile.js做了三個改變

  1. browserify 外面包覆了一層 watchify
  2. 我們監聽了 watchedBrowserify 的 update Event 每次修改的時候就會自動重新打包並產生新增檔案到 dist 到資料夾內
  3. 我們也監聽了 log 的 Event 使用 gulp-util 的 log 來做紀錄顯示

綜合以上 1, 2 的步驟我們將 browserify 移出了 default task 放到了 function bundle裡然後透過 監聽 update 的 Event 來隨時重新編譯程式

而 3 則是會列印出過程的訊息方便我們開發程式的時候查閱

然後我們開始啟動則會看到下方的訊息

1
2
3
4
5
16:31:53] Starting 'copy-html'...
[16:31:53] Finished 'copy-html' after 15 ms
[16:31:53] Starting 'default'...
[16:31:54] 2690 bytes written (0.09 seconds)
[16:31:54] Finished 'default' after 1.12 s

並且當你修改 main.ts 時會自動重新編譯

也就是當你重新 refresh 網頁的時候

就可以看到最新的更新狀態

Uglify

因為 Uglify 會撕裂你的程式碼,所以需要安裝 vinyl-buffergulp-sourcemaps 來讓 sourcemaps 持續動作

1
$ yarn add gulp-uglify vinyl-buffer gulp-sourcemaps -D

gulpfile.js修改為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const gulp = require("gulp");
const browserify = require("browserify");
const source = require("vinyl-source-stream");
const tsify = require("tsify");
const watchify = require("watchify");
const gutil = require("gulp-util");
const uglify = require("gulp-uglify");
const sourcemaps = require("gulp-sourcemaps");
const buffer = require("vinyl-buffer");

const watchedBrowserify = watchify(
browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {}
})
).plugin(tsify);

const paths = {
pages: ["src/*.html"]
};

gulp.task("copy-html", () => {
return gulp.src(paths.pages).pipe(gulp.dest("dist"));
});

function bundle() {
return watchedBrowserify
.bundle()
.pipe(source("bundle.js"))
.pipe(gulp.dest("dist"));
}

gulp.task("default", ["copy-html"], bundle);

gulp.task("production", ['copy-html'], () => {
return browserify({
basedir: ".",
debug: true,
entries: ["src/main.ts"],
cache: {},
packageCache: {}
})
.plugin(tsify)
.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(uglify())
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest("dist"));
});

watchedBrowserify.on("update", bundle);
watchedBrowserify.on("log", gutil.log);
1
$ gulp production

Note: Uglify 只需要做一次,buffer和 sourcemaps 會產生一個獨立的檔案 bundle.map.js 你也可以確認 bundle.js 中的程式事不是已經最小化了

TypeScript-TypeScriptIn5Minutes

翻譯來源

TypeScript

TypeScript in 5 minutes

使用 TypeScript 建立第一個簡單的 網頁應用

Installing TypeScript

有兩個主要的安裝方式

  • 使用 NPM 安裝
  • 安裝 [Visual Studio plugins]

Visual Studio

Visual Studio 2017 和 Visual Studio 2015 預設已經有使用 TypeScript ,但若是你不希望安裝 Visual Studio 你也可以安裝 TypeScript

NPM

1
$ npm install -g typescript

建立你的第一個 TypeScript 檔案

greeter.ts

1
2
3
4
5
6
7
function greeter(person) {
return "Hello, " + person;
}

var user = "Jane User";

document.body.innerHTML = greeter(user);

Compiling your code

1
$ tsc greeter.ts

經過編譯,你會多了一個 greeter.js 的 Javascript 檔案

而這個檔案可以在前端或 Nodejs 中使用

接下來我們要開始接下來我們要開始嘗試 TypeScript 的幾個功能

就是先在 Function greeter 中的 person 這個參數加上類別的識別

1
2
3
4
5
6
7
function greeter(person: string) {
return "Hello, " + person;
}
// var user = "Jane User";
var user = [0, 1, 2];

document.body.innerHTML = greeter(user);

當編譯的時候就會看到一個錯誤

1
greeter.ts(7,35): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

同樣的若是你使用 Function greeter 的時候移除了參數

1
2
3
4
5
function greeter(person: string) {
return "Hello, " + person;
}

document.body.innerHTML = greeter();

你也會得到一個錯誤訊息

1
greeter.ts(7,27): error TS2554: Expected 1 arguments, but got 0.

TypeScript 會協助分析你的程式並且提醒你還需要提供相關的資訊

雖然這些錯誤訊息提醒,但是你的 greeter.js 仍然會產生 TypeScript 僅僅會警告你這些程式碼執行會得到不是你的預期結果

interfaces

然後我們可以進一步的開發我們的範例,我們增加一個 interface 他描述了一個物件擁有兩個欄位 firstName
lastName

TypeScript 中容許一個結構內擁有兩種類型只需要描述物件的輪廓

而不需要過於明確的解釋物件的值

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
firstName: string;
lastName: string;
}

function greeter(person: Person) {
return "Hello, " + person;
}

var user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);

Classes

最後我在做一些延伸

TypeScript 支援 Javascript 使用 class-based object-oriented programming

Javascipt 的物件導向設計

我們再多建立一個 Student 的 class 中擁有 constructor 和一些 public 的欄位

讓 classes 和 interface 可以很好的共同使用,並且讓開發者可以選擇正確的抽象化他的程式碼

1
constuctor 是一個簡潔並且方便我們建立 object 的參數 的函式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student {
fullName: string;
constructor(
public firstName: string,
public middleInitial: string,
public lastName: string
) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}

interface Person {
firstName: string;
lastName: string;
}

function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

var user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);
1
$ tsc greeter.ts

然後你可以建立一個 greeter.html

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head><title>TypeScript Greeter</title></head>
<body>
<script src="greeter.js"></script>
</body>
</html>

然後使用瀏覽器開啟這個 HTML 檔案 就可以看到相關的結果

Leedcode-Q7

Hamming Distansce (漢明距離)

定義

將一個字符串變換成另一個字符串所需要替換的字符個數

題目

1
2
3
4
5
6
7
8
9
10
11
12
13

0 ≤ x, y < 231.

Input: x = 1, y = 4

Output: 2

Explanation:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑

The above arrows point to positions where the corresponding bits are different.

將 1 轉為二進位的字串則是 1, 4 轉為二進位的字串則是 1000

但是計算漢明距離時必須是兩個等長的符號

所以必須幫 1 補零為 0001

0001

1000

所以需要替換兩個字符

所以回傳值為 2

Javascript 的位元運算子

^

XOR assignment

將數字轉為二進位之後再做比對的計算

Example

Rule

3 ^ 5 = 6

011

101


110

再將 110 轉為十進制則為 6

也就是說當當兩個同位元做比對的規則是

數值相同則為 0 不相同則為 1

我們可以和另外一個運算子 | 做比較

因為在官方的範例中

他們的結果都是一樣的

但是測試之後結過卻還是不一樣

Example 2

3 | 5 = 7

Rule

011

101


111

再將 111 轉為十進制則為 7

也就是 OR 的比對規則是只要有一個為 1 就等於 1

&

Bitwise AND assignment

Rule

也就是說如果同位元的都為 1 才會等於 1

否則其餘狀況皆為零

Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @param {number} x
* @param {number} y
* @return {number}
*/
var hammingDistance = function(x, y) {
let ans = 0;
while(x || y){
ans += (x & 1) ^ (y & 1);
x >>= 1;
y >>= 1;
}
return ans;
};

利用 while 迴圈

x & 1 與 y & 1 會取得最後一位檢查是否為零

若是 0 則為 0, 若為 1 則為 1

其結果在使用 XOR(^) 來作判斷是否相同

若同為 0 或 1 則回傳 0

若一個為 0 一個為 1 的時候回傳 1

將結果疊加在 ans 上

最後再將 x y 做Right shift

之後再將 ans 回傳即為我們需要的結果

參考資料

漢明距離

漢明重量

Javascript 運算子

NPM publish with gulp

NPM Publish

Initial Project

1
2
3
4
5
6
7
8
9
10
$ mkdir publishDemo
$ yarn init
// question name (publishDemo):
// question version (1.0.0):
// question description: publishdemo
// question entry point (index.js):
// question repository url:
// question author: Tomas
// question license (MIT):
// success Saved package.json

初始化結束後會增加一個 package.json

1
2
3
4
5
6
7
8
{
"name": "publishDemo",
"version": "1.0.0",
"description": "publishdemo",
"main": "index.js",
"author": "Tomas",
"license": "MIT"
}

Create index.js

1
module.exports = 'hello world';

publish

1
2
$ npm login
// 登入 NPM 的帳號並且驗證信箱
1
2
$ npm publish
+ publishdemotest@1.0.0

npm publish success

add Gulp

gulpfile.js

1
$ mkdir src && mv index.js ./src
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const gulp = require("gulp");
const babel = require("gulp-babel");

gulp.task("compile", () => {
return gulp
.src("./src/**/*.js")
.pipe(
babel({
presets: ["es2015", "stage-2"],
plugins: []
})
)
.pipe(gulp.dest("./build"));
});

gulp.task("default", ["compile"]);

1
2
3
4
5
6
7
8
9
10
$ gulp
// result
.
├── build
│   └── index.js
├── gulpfile.js
├── package.json
├── src
│   └── index.js
└── yarn.lock

Git

.gitignore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

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

# Coverage directory used by tools like istanbul
coverage

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

# node-waf configuration
.lock-wscript

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

# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
build

publishdemo

update NPM version and main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "publishdemotest",
"version": "1.0.1",
"description": "publishdemo",
"main": "./build/index.js",
"author": "Tomas",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"gulp": "^3.9.1",
"gulp-babel": "^7.0.0"
}
}

重新 publish

1
2
$ npm publish
//+ publishdemotest@1.0.1

這時如果我在其他專案從 NPM 下載結果如下圖

1
$ yarn add publishdemotest
1
2
3
4
5
6
7
8
9
  $ tree ./node_modules/publishdemotest
//result

./node_modules/publishdemotest
├── gulpfile.js
├── package.json
├── src
│   └── index.js
└── yarn.lock

並沒有build 這個 folder

但是因為在 package.json 中我們將進入點設定為 ./build/index.js

所以會造成我們引入時的錯誤

NPM 會自動將 .gitignore 設定為預設的設定檔案

但是我們希望可以有其他的設定

我們可以增加一個 .npmignore

1
2
3
4
5
6
7
# Logs
logs
*.log
npm-debug.log*
node_modules

*error*

我們將build 移除在 .npmignore 中

再將 package.json 的version 升級

再重新publish 一次

在 Demo 的 project 升級 package

1
$ yarn upgrade publishdemotest
1
2
3
4
5
6
7
8
9
10
$ tree node_modules/publishdemotest

node_modules/publishdemotest
├── build
│   └── index.js
├── gulpfile.js
├── package.json
├── src
│   └── index.js
└── yarn.lock

透過 .npmignore 就可以達到我們需要的效果

同樣如果不希望使用者還需要安裝相關套件

也可以把 node_modules 移除 .npmignore 中

如此使用者就不需要安裝相關套件

也可以達到鎖定版本的效果

ReactUnitestWithJest

Setup

Create React App

Run the test

Without Create React App

1
$ npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer

.babelrc

1
2
3
{
"presets": ["es2015", "react"]
}

先寫一個 React Component Link Example

Link.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React from 'react';

const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};

export default class Link extends React.Component {

constructor(props) {
super(props);

this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);

this.state = {
class: STATUS.NORMAL,
};
}

_onMouseEnter() {
this.setState({class: STATUS.HOVERED});
}

_onMouseLeave() {
this.setState({class: STATUS.NORMAL});
}

render() {
return (
<a
className={this.state.class}
href={this.props.page || '#'}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}>
{this.props.children}
</a>
);
}

}

__tests__/Link.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

test('Link changes the class when hovered', () => {
const component = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();

// manually trigger the callback
tree.props.onMouseEnter();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();

// manually trigger the callback
tree.props.onMouseLeave();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();

__tests__/__snashots__/Link.test.js.snap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Link changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

exports[`Link changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

exports[`Link changes the class when hovered 3`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

測試的時候每次呼叫 toMatchSnapshot 的時候

會依序會依序在 __tests__/__snashots__/Link.test.js.snap

取得取得 Mock 做比對

需要完全吻合才會回傳正確

DOM Testing

上述的範例是單純比較依據不同的 Input 後造成的 Component 比對

若是你需要操作這些實體化的 Component 則可以使用 Enzyme 或是 React 的

TestUtils

而下述範例則使用 Enzyme

Example

CheckboxWithLabel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react';

export default class CheckboxWithLabel extends React.Component {

constructor(props) {
super(props);
this.state = {isChecked: false};

// bind manually because React class components don't auto-bind
// http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
this.onChange = this.onChange.bind(this);
}

onChange() {
this.setState({isChecked: !this.state.isChecked});
}

render() {
return (
<label>
<input
type="checkbox"
checked={this.state.isChecked}
onChange={this.onChange}
/>
{this.state.isChecked ? this.props.labelOn : this.props.labelOff}
</label>
);
}
}

使用 Enzyme 的 shallow renderer

__tests__/CheckboxWithdLabel.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import React from 'react';
import {shallow} from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';

test('CheckboxWithLabel changes the text after click', () => {
// Render a checkbox with label in the document
const checkbox = shallow(
<CheckboxWithLabel labelOn="On" labelOff="Off" />
);

expect(checkbox.text()).toEqual('Off');

checkbox.find('input').simulate('change');

expect(checkbox.text()).toEqual('On');
});

參考資料

jest Testing React Apps

Python-Decorator

Decorator

在網路上最常看到的範例

Example

1
2
3
4
5
6
7
8
9
10
11
12
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ + " was called"
return func(*args, **kwargs)
return with_logging

@logged
def f(x):
return x + x * x


print f(2)

logger 中定義一個 Function with_logging

在執行前 f 會被當成參數 func 傳入 logged

with_logging 會優先被執行

執行後才會執行 func

上面的程式碼也會等於

1
2
3
4
5
6
7
8
9
10
11
12
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ + " was called"
return func(*args, **kwargs)
return with_logging

def f(x):
return x + x * x

f = logged(f)

print f(2)

執行後的結果是一樣的

看起來要使用 Decorator 所有的操作都會在 Function 上

傳入與回傳值都是 Function

再來就是如果有多個裝飾子

那麼順序又如何決定的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def makebold(fn):
def wrapper():
return "<b>" + fn() + "</b>"
return wrapper


def makeitalic(fn):
def wrapper():
return "<i>" + fn() + "</i>"
return wrapper


@makeitalic
@makebold
def hello():
return "hello world"

print hello()

# result: <i><b>hello world</b></i>

可以看到順序會先包 makeblod 然後再來才是 makeitalic

執行順序是由下往上

也可以把 decorator 當成是 recurcive 理解

# Class 的 Decorator

Example

class entryExit(object):
    def __init__(self, f):
        print 'entry init enter'
        self.f = f
        print 'entry init exit'

    def __call__(self, *args):
        print 'Entering', self.f.__name__
        r = self.f(*args)
        print 'Exit', self.f.__name__
        return r
print 'decorator using'

@entryExit
def hello(a):
    print 'inside hello'
    return 'hello world' + a

print 'test start'
print hello('friends')

'''
result:
---------------------
decorator using
entry init enter
entry init exit
test start
Entering hello
inside hello
Exit hello
hello worldfriends
'''

比較一開始的函式例子跟後來的類別例子

雖然識別字指的一個是類別

一個是函數

在程式碼中

在函式名後面加上()

變成函式呼叫

而類別本來是不能被呼叫的(not callable)

但加上類別方法__call__之後

就變得可以被呼叫

從程式的結果來看 test start 出現在 entry init exit 的後面

代表在 print 'test start' 之前

entryExit 就已實例化

應該就是 @entryExit 這句執行的

當程式執行到 hello('friend') 的時候

先進入的是 entryExit 的 __call__

後來才是 hello 自己的內容

這樣的流程觀察比較清楚

但是 cpython 倒底如何實作這段

這個例子很漂亮的介紹兩件事

第一件事是 decorator

第二件事就是 python 函式與物件之間的巧妙關聯

Debugging-Nodejs-With-VSCode

環境

  • Nodejs - 7.9.10
  • VSCode - 1.12.2

Download VSCode

vscode

Image

點擊這個 Icon 後再點擊齒輪會自動產生 launch.json

launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"protocol": "inspector",
"program": "${workspaceRoot}/index.js",
"cwd": "${workspaceRoot}",
"runtimeArgs": [
"--nolazy",
"--inspect-brk"
],
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development"
}
}
]
}
  • console - 在Terminal 中啟動程式
  • env - 設定動態變數,可以傳入程式中使用
  • runtimeArgs - 動態參數
  • program - 啟動程式路徑
  • protocol - npm 8 有兩種不同的protocol ,  預設是 legacy

檢查launch.json

debug

開始 debug 後上方會多一條控制 bar

debug

使用 postman 發送 Request

debug

因為我有 console.log 所以在 Debug Console 中會把 login 顯示出來

debug

NodeDesignPatten-06

Design Patten

設計模式是關於程式碼重複使用的問題

推薦書籍

Design Pattern List

  • Factory
  • Revealing constructor
  • Proxy
  • Decorator
  • Adapter
  • Strategy
  • State
  • Template
  • Middleware
  • Command

Factory

在 Javascript 中 設計通常以可用,簡單並且模組化開發為主旨

而使用 Factory 則可以在物件導向上包覆一層 Wrapper

使用上可以更加彈性

在 Factory 中可以透過 Object.create() 來建立一個物件

或是利用特定的條件來建立不同的物件

Example

factory.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ImageJpeg = require('./imageJpeg');
const ImageGif = require('./imageGif');
const ImagePng = require('./imagePng');

function createImage(name) {
if (name.match(/\.jpe?g$/)) {
return new ImageJpeg(name);
} else if (name.match(/\.gif$/)) {
return new ImageGif(name);
} else if (name.match(/\.png$/)) {
return new ImagePng(name);
} else {
throw new Exception('Unsupported format');
}
}

const image1 = createImage('photo.jpg');
const image2 = createImage('photo.gif');
const image3 = createImage('photo.png');

console.log(image1, image2, image3);

imageJpeg.js

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

const Image = require('./image');

module.exports = class ImageJpg extends Image {
constructor(path) {
if (!path.match(/\.jpe?g$/)) {
throw new Error(`${path} is not a JPEG image`);
}
super(path);
}
};

imagePng.js

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

const Image = require('./image');

module.exports = class ImagePng extends Image {
constructor(path) {
if (!path.match(/\.png$/)) {
throw new Error(`${path} is not a PNG image`);
}
super(path);
}
};

imageGif.js

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

const Image = require('./image');

module.exports = class ImageGif extends Image {
constructor(path) {
if (!path.match(/\.gif/)) {
throw new Error(`${path} is not a GIF image`);
}
super(path);
}
};

封裝

封裝是防止某些程式碼被外部直接飲用或更改時使用

也稱為訊息隱藏

而 Javascript 常用 Closure 來實現

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createPerson(name) {
const privateProperties = {};
const person = {
setName: name => {
if (!name) throw new Error('A person must have a name');
privateProperties.name = name;
},
getName: () => privateProperties.name
};

person.setName(name);
return person;
}

const person = createPerson('Tomas Lin');
console.log(person.getName(), person);

此範例中的person 只有透過 setName與 getName可以對person 取值與修改

1
2
3
4
5
6
7
8
9
使用 Factory 只是其中一個方式

另外也有不成文約定

function 前加上 **_** 或 **$**

但是這樣依舊可以在外部呼叫

另外也可以使用 [WeakMap](http://fitzgeraldnick.com/2014/01/13/hiding-implementation-details-with-e6-weakmaps.html)

建立一個完整的 Factory Demo

profiler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Profiler {
constructor(label) {
this.label = label;
this.lastTime = null;
}

start() {
this.lastTime = process.hrtime();
}
end() {
const diff = process.hrtime(this.lastTime);
console.log(
`Timer "${this.label}" took ${diff[0]} seconds and ${diff[1]} nanoseconds.`
);
}
}

module.exports = function (label) {
if (process.env.NODE_ENV === 'development') {
return new Profiler(label); //[1]
} else if (process.env.NODE_ENV === 'production') {
return { //[2]
start: function () { },
end: function () { }
}
} else {
throw new Error('Must set NODE_ENV');
}
}

profilerTest.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const profiler = require('./profile');

function getRandomArray(len) {
const p = profiler(`Generating a ${len} items long array`);
p.start();
const arr = [];
for (let i = 0; i < len; i++) {
arr.push(Math.random());
}
p.end();
}

getRandomArray(1e6);
console.log('Done');
1
$ NODE_ENV=development node profilerTest
1
$ NODE_ENV=production node profilerTest

組合工廠

利用遊戲的方式來解說

通常一種遊戲會有多種角色

每種角色會擁有各自不同的基本能力

  • Runner: 可移動
  • Samurai: 可移動並且攻擊
  • Sniper: 可射擊但不可移動
  • Gunslinger: 可移動並且射擊
  • Western Samurai: 可以移動 攻擊 並射擊

希望上述可以自由地互相結合

所以不能使用 Class 或是 inheritance 來解決

Example

使用套件stampit

|