TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。
本文阿寶哥將分享這些年在學習 TypeScript 過程中,遇到的 10 大 “奇怪” 的符號。其中有一些符號,阿寶哥第一次見的時候也覺得 “一臉懵逼”,希望本文對學習 TypeScript 的小伙伴能有一些幫助。
好的,下面我們來開始介紹第一個符號 —— ! 非空斷言操作符。
一、! 非空斷言操作符
在上下文中當類型檢查器無法斷定類型時,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。
那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場景。
1.1 忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) { // Type 'string | null | undefined' is not assignable to type 'string'. // Type 'undefined' is not assignable to type 'string'. const onlyString: string = maybeString; // Error const ignoreUndefinedAndNull: string = maybeString!; // Ok }
1.2 調用函數時忽略 undefined 類型
type NumGenerator = () => number; function myFunc(numGenerator: NumGenerator | undefined) { // Object is possibly 'undefined'.(2532) // Cannot invoke an object which is possibly 'undefined'.(2722) const num1 = numGenerator(); // Error const num2 = numGenerator!(); //OK }
因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除,所以在實際使用的過程中,要特別注意。比如下面這個例子:
const a: number | undefined = undefined; const b: number = a!; console.log(b);
以上 TS 代碼會編譯生成以下 ES5 代碼:
"use strict"; const a = undefined; const b = a; console.log(b);
雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執行以上代碼,在控制臺會輸出 undefined。
二、?. 運算符
TypeScript 3.7 實現了呼聲最高的 ECMAScript 功能之一:可選鏈(Optional Chaining)。有了可選鏈后,我們編寫代碼時如果遇到 null 或 undefined 就可以立即停止某些表達式的運行??蛇x鏈的核心是新的 ?. 運算符,它支持以下語法:
obj?.prop
obj?.[expr]
arr?.[index] func?.(args)
這里我們來舉一個可選的屬性訪問的例子:
const val = a?.b;
為了更好的理解可選鏈,我們來看一下該 const val = a?.b 語句編譯生成的 ES5 代碼:
var val = a === null || a === void 0 ? void 0 : a.b;
上述的代碼會自動檢查對象 a 是否為 null 或 undefined,如果是的話就立即返回 undefined,這樣就可以立即停止某些表達式的運行。你可能已經想到可以使用 ?. 來替代很多使用 && 執行空檢查的代碼:
if(a && a.b) { } if(a?.b){ } /**
* if(a?.b){ } 編譯后的ES5代碼
*
* if(
* a === null || a === void 0
* ? void 0 : a.b) {
* }
*/
但需要注意的是,?. 與 && 運算符行為略有不同,&& 專門用于檢測 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只會驗證對象是否為 null 或 undefined,對于 0 或空字符串來說,并不會出現 “短路”。
2.1 可選元素訪問
可選鏈除了支持可選屬性的訪問之外,它還支持可選元素的訪問,它的行為類似于可選屬性的訪問,只是可選元素的訪問允許我們訪問非標識符的屬性,比如任意字符串、數字索引和 Symbol:
function tryGetArrayElement<T>(arr?: T[], index: number = 0) { return arr?.[index];
}
以上代碼經過編譯后會生成以下 ES5 代碼:
"use strict"; function tryGetArrayElement(arr, index) { if (index === void 0) { index = 0; } return arr === null || arr === void 0 ? void 0 : arr[index];
}
通過觀察生成的 ES5 代碼,很明顯在 tryGetArrayElement 方法中會自動檢測輸入參數 arr 的值是否為 null 或 undefined,從而保證了我們代碼的健壯性。
2.2 可選鏈與函數調用
當嘗試調用一個可能不存在的方法時也可以使用可選鏈。在實際開發過程中,這是很有用的。系統中某個方法不可用,有可能是由于版本不一致或者用戶設備兼容性問題導致的。函數調用時如果被調用的方法不存在,使用可選鏈可以使表達式自動返回 undefined 而不是拋出一個異常。
可選調用使用起來也很簡單,比如:
let result = obj.customMethod?.();
該 TypeScript 代碼編譯生成的 ES5 代碼如下:
var result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);
另外在使用可選調用的時候,我們要注意以下兩個注意事項:
如果存在一個屬性名且該屬性名對應的值不是函數類型,使用 ?. 仍然會產生一個 TypeError 異常。
可選鏈的運算行為被局限在屬性的訪問、調用以及元素的訪問 —— 它不會沿伸到后續的表達式中,也就是說可選調用不會阻止 a?.b / someMethod() 表達式中的除法運算或 someMethod 的方法調用。
三、?? 空值合并運算符
在 TypeScript 3.7 版本中除了引入了前面介紹的可選鏈 ?. 之外,也引入了一個新的邏輯運算符 —— 空值合并運算符 ??。當左側操作數為 null 或 undefined 時,其返回右側的操作數,否則返回左側的操作數。
與邏輯或 || 運算符不同,邏輯或會在左操作數為 falsy 值時返回右側操作數。也就是說,如果你使用 || 來為某些變量設置默認的值時,你可能會遇到意料之外的行為。比如為 falsy 值(''、NaN 或 0)時。
這里來看一個具體的例子:
const foo = null ?? 'default string'; console.log(foo); // 輸出:"default string" const baz = 0 ?? 42; console.log(baz); // 輸出:0
以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict"; var _a, _b; var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo); // 輸出:"default string" var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 輸出:0
通過觀察以上代碼,我們更加直觀的了解到,空值合并運算符是如何解決前面 || 運算符存在的潛在問題。下面我們來介紹空值合并運算符的特性和使用時的一些注意事項。
3.1 短路
當空值合并運算符的左表達式不為 null 或 undefined 時,不會對右表達式進行求值。
function A() { console.log('A was called'); return undefined;} function B() { console.log('B was called'); return false;} function C() { console.log('C was called'); return "foo";} console.log(A() ?? C()); console.log(B() ?? C());
上述代碼運行后,控制臺會輸出以下結果:
A was called
C was called
foo
B was called
false
3.2 不能與 && 或 || 操作符共用
若空值合并運算符 ?? 直接與 AND(&&)和 OR(||)操作符組合使用 ?? 是不行的。這種情況下會拋出 SyntaxError。
// '||' and '??' operations cannot be mixed without parentheses.(5076) null || undefined ?? "foo"; // raises a SyntaxError // '&&' and '??' operations cannot be mixed without parentheses.(5076) true && undefined ?? "foo"; // raises a SyntaxError
但當使用括號來顯式表明優先級時是可行的,比如:
(null || undefined ) ?? "foo"; // 返回 "foo"
3.3 與可選鏈操作符 ?. 的關系
空值合并運算符針對 undefined 與 null 這兩個值,可選鏈式操作符 ?. 也是如此??蛇x鏈式操作符,對于訪問屬性可能為 undefined 與 null 的對象時非常有用。
interface Customer {
name: string;
city?: string;
} let customer: Customer = {
name: "Semlinker" }; let customerCity = customer?.city ?? "Unknown city"; console.log(customerCity); // 輸出:Unknown city
前面我們已經介紹了空值合并運算符的應用場景和使用時的一些注意事項,該運算符不僅可以在 TypeScript 3.7 以上版本中使用。當然你也可以在 JavaScript 的環境中使用它,但你需要借助 Babel,在 Babel 7.8.0 版本也開始支持空值合并運算符。
四、?: 可選屬性
在面向對象語言中,接口是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。 TypeScript 中的接口是一個非常靈活的概念,除了可用于對類的一部分行為進行抽象以外,也常用于對「對象的形狀(Shape)」進行描述。
在 TypeScript 中使用 interface 關鍵字就可以聲明一個接口:
interface Person {
name: string;
age: number;
} let semlinker: Person = {
name: "semlinker",
age: 33,
};
在以上代碼中,我們聲明了 Person 接口,它包含了兩個必填的屬性 name 和 age。在初始化 Person 類型變量時,如果缺少某個屬性,TypeScript 編譯器就會提示相應的錯誤信息,比如:
// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741) let lolo: Person = { // Error name: "lolo" }
為了解決上述的問題,我們可以把某個屬性聲明為可選的:
interface Person {
name: string;
age?: number;
} let lolo: Person = {
name: "lolo" }
4.1 工具類型
4.1.1 Partial<T>
在實際項目開發過程中,為了提高代碼復用率,我們可以利用 TypeScript 內置的工具類型 Partial<T> 來快速把某個接口類型中定義的屬性變成可選的:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} /**
* type PullDownRefreshOptions = {
* threshold?: number | undefined;
* stop?: number | undefined;
* }
*/ type PullDownRefreshOptions = Partial<PullDownRefreshConfig>
是不是覺得 Partial<T> 很方便,下面讓我們來看一下它是如何實現的:
/**
* Make all properties in T optional
*/ type Partial<T> = {
[P in keyof T]?: T[P];
};
4.1.2 Required<T>
既然可以快速地把某個接口中定義的屬性全部聲明為可選,那能不能把所有的可選的屬性變成必選的呢?答案是可以的,針對這個需求,我們可以使用 Required<T> 工具類型,具體的使用方式如下:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} type PullDownRefreshOptions = Partial<PullDownRefreshConfig> /**
* type PullDownRefresh = {
* threshold: number;
* stop: number;
* }
*/ type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>
同樣,我們來看一下 Required<T> 工具類型是如何實現的:
/**
* Make all properties in T required
*/ type Required<T> = {
[P in keyof T]-?: T[P];
};
原來在 Required<T> 工具類型內部,通過 -? 移除了可選屬性中的 ?,使得屬性從可選變為必選的。
五、& 運算符
在 TypeScript 中交叉類型是將多個類型合并為一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。
type PartialPointX = { x: number; }; type Point = PartialPointX & { y: number; }; let point: Point = {
x: 1,
y: 1 }
在上面代碼中我們先定義了 PartialPointX 類型,接著使用 & 運算符創建一個新的 Point 類型,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point 類型的變量并初始化。
5.1 同名基礎類型屬性的合并
那么現在問題來了,假設在合并多個類型的過程中,剛好出現某些類型存在相同的成員,但對應的類型又不一致,比如:
interface X {
c: string;
d: string;
} interface Y {
c: number;
e: string } type XY = X & Y; type YX = Y & X; let p: XY; let q: YX;
在上面的代碼中,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致。對于這種情況,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 string 或 number 類型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
為什么接口 X 和接口 Y 混入后,成員 c 的類型會變成 never 呢?這是因為混入后成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以混入后成員 c 的類型為 never。
5.2 同名非基礎類型屬性的合并
在上面示例中,剛好接口 X 和接口 Y 中內部成員 c 的類型都是基本數據類型,那么如果是非基本數據類型的話,又會是什么情形。我們來看個具體的例子:
interface D { d: boolean; } interface E { e: string; } interface F { f: number; } interface A { x: D; } interface B { x: E; } interface C { x: F; } type ABC = A & B & C; let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666 }
}; console.log('abc:', abc);
以上代碼成功運行后,控制臺會輸出以下結果:
由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那么是可以成功合并。
六、| 分隔符
在 TypeScript 中聯合類型(Union Types)表示取值可以為多種類型中的一種,聯合類型使用 | 分隔每個類型。聯合類型通常與 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => { /* ... */ };
以上示例中 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給 sayHello 函數。
sayHello("semlinker");
sayHello(undefined);
此外,對于聯合類型來說,你可能會遇到以下的用法:
let num: 1 | 2 = 1; type EventNames = 'click' | 'scroll' | 'mousemove';
示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。
6.1 類型保護
當使用聯合類型時,我們必須盡量把當前值的類型收窄為當前值的實際類型,而類型保護就是實現類型收窄的一種手段。
類型保護是可執行運行時檢查的一種表達式,用于確保該類型在一定的范圍內。換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數字。類型保護與特性檢測并不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。
目前主要有四種的方式來實現類型保護:
6.1.1 in 關鍵字
interface Admin {
name: string;
privileges: string[];
} interface Employee {
name: string;
startDate: Date;
} type UnknownEmployee = Employee | Admin; function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges" in emp) { console.log("Privileges: " + emp.privileges);
} if ("startDate" in emp) { console.log("Start Date: " + emp.startDate);
}
}
6.1.2 typeof 關鍵字
function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value;
} if (typeof padding === "string") { return padding + value;
} throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 類型保護只支持兩種形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必須是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。
6.1.3 instanceof 關鍵字
interface Padder {
getPaddingString(): string;
} class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) {}
getPaddingString() { return Array(this.numSpaces + 1).join(" ");
}
} class StringPadder implements Padder { constructor(private value: string) {}
getPaddingString() { return this.value;
}
} let padder: Padder = new SpaceRepeatingPadder(6); if (padder instanceof SpaceRepeatingPadder) { // padder的類型收窄為 'SpaceRepeatingPadder' }
6.1.4 自定義類型保護的類型謂詞(type predicate)
function isNumber(x: any): x is number { return typeof x === "number";
} function isString(x: any): x is string { return typeof x === "string";
}
七、_ 數字分隔符
TypeScript 2.7 帶來了對數字分隔符的支持,正如數值分隔符 ECMAScript 提案中所概述的那樣。對于一個數字字面量,你現在可以通過把一個下劃線作為它們之間的分隔符來分組數字:
const inhabitantsOfMunich = 1_464_301; const distanceEarthSunInKm = 149_600_000; const fileSystemPermission = 0b111_111_000; const bytes = 0b1111_10101011_11110000_00001101;
分隔符不會改變數值字面量的值,但邏輯分組使人們更容易一眼就能讀懂數字。以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict"; var inhabitantsOfMunich = 1464301; var distanceEarthSunInKm = 149600000; var fileSystemPermission = 504; var bytes = 262926349;
7.1 使用限制
雖然數字分隔符看起來很簡單,但在使用時還是有一些限制。比如你只能在兩個數字之間添加 _ 分隔符。以下的使用方式是非法的:
// Numeric separators are not allowed here.(6188) 3_.141592 // Error 3._141592 // Error // Numeric separators are not allowed here.(6188) 1_e10 // Error 1e_10 // Error // Cannot find name '_126301'.(2304) _126301 // Error // Numeric separators are not allowed here.(6188) 126301_ // Error // Cannot find name 'b111111000'.(2304) // An identifier or keyword cannot immediately follow a numeric literal.(1351) 0_b111111000 // Error // Numeric separators are not allowed here.(6188) 0b_111111000 // Error
當然你也不能連續使用多個 _ 分隔符,比如:
// Multiple consecutive numeric separators are not permitted.(6189) 123__456 // Error
7.2 解析分隔符
此外,需要注意的是以下用于解析數字的函數是不支持分隔符:
Number()
parseInt()
parseFloat()
這里我們來看一下實際的例子:
Number('123_456') NaN parseInt('123_456') 123 parseFloat('123_456') 123
很明顯對于以上的結果不是我們所期望的,所以在處理分隔符時要特別注意。當然要解決上述問題,也很簡單只需要非數字的字符刪掉即可。這里我們來定義一個 removeNonDigits 的函數:
const RE_NON_DIGIT = /[^0-9]/gu; function removeNonDigits(str) {
str = str.replace(RE_NON_DIGIT, ''); return Number(str);
}
該函數通過調用字符串的 replace 方法來移除非數字的字符,具體的使用方式如下:
removeNonDigits('123_456') 123456 removeNonDigits('149,600,000') 149600000 removeNonDigits('1,407,836') 1407836
八、<Type> 語法
8.1 TypeScript 斷言
有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。
類型斷言有兩種形式:
8.1.1 “尖括號” 語法
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;
8.1.2 as 語法
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
8.2 TypeScript 泛型
對于剛接觸 TypeScript 泛型的讀者來說,首次看到 <T> 語法會感到陌生。其實它沒有什么特別,就像傳遞參數一樣,我們傳遞了我們想要用于特定函數調用的類型。
參考上面的圖片,當我們調用 identity<Number>(1) ,Number 類型就像參數 1 一樣,它將在出現 T 的任何位置填充該類型。圖中 <T> 內部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數的類型占位符,同時它被分配給 value 參數用來代替它的類型:此時 T 充當的是類型,而不是特定的 Number 類型。
其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
K(Key):表示對象中的鍵類型;
V(Value):表示對象中的值類型;
E(Element):表示元素類型。
其實并不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用于擴展我們定義的 identity 函數:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity<Number, string>(68, "Semlinker"));
除了為類型變量顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔。我們可以完全省略尖括號,比如:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity(68, "Semlinker"));
對于上述代碼,編譯器足夠聰明,能夠知道我們的參數類型,并將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。
九、@XXX 裝飾器
9.1 裝飾器語法
對于一些剛接觸 TypeScript 的小伙伴來說,在第一次看到 @Plugin({...}) 這種語法可能會覺得很驚訝。其實這是裝飾器的語法,裝飾器的本質是一個函數,通過裝飾器我們可以方便地定義與對象相關的元數據。
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}) @Injectable() export class Device extends IonicNativePlugin {}
在以上代碼中,我們通過裝飾器來保存 ionic-native 插件的相關元信息,而 @Plugin({...}) 中的 @ 符號只是語法糖,為什么說是語法糖呢?這里我們來看一下編譯生成的 ES5 代碼:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r;
}; var Device = /** @class */ (function (_super) {
__extends(Device, _super); function Device() { return _super !== null && _super.apply(this, arguments) || this;
}
Device = __decorate([
Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}),
Injectable()
], Device); return Device;
}(IonicNativePlugin));
通過生成的代碼可知,@Plugin({...}) 和 @Injectable() 最終會被轉換成普通的方法調用,它們的調用結果最終會以數組的形式作為參數傳遞給 __decorate 函數,而在 __decorate 函數內部會以 Device 類作為參數調用各自的類型裝飾器,從而擴展對應的功能。
9.2 裝飾器的分類
在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和參數裝飾器四大類。
9.2.1 類裝飾器
類裝飾器聲明:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:
target: TFunction - 被裝飾的類
看完第一眼后,是不是感覺都不好了。沒事,我們馬上來個例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void { console.log("Hello Semlinker!");
};
} @Greeter class Greeting { constructor() { // 內部實現 }
} let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';
上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器。
友情提示:讀者可以直接復制上面的代碼,在 TypeScript Playground 中運行查看結果。
9.2.2 屬性裝飾器
屬性裝飾器聲明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 被裝飾類的屬性名
趁熱打鐵,馬上來個例子熱熱身:
function logProperty(target: any, key: string) { delete target[key]; const backingField = "_" + key; Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true }); // property getter const getter = function (this: any) { const currVal = this[backingField]; console.log(`Get: ${key} => ${currVal}`); return currVal;
}; // property setter const setter = function (this: any, newVal: any) { console.log(`Set: ${key} => ${newVal}`); this[backingField] = newVal;
}; // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter,
enumerable: true,
configurable: true });
} class Person { @logProperty public name: string; constructor(name : string) { this.name = name;
}
} const p1 = new Person("semlinker");
p1.name = "kakuqo";
以上代碼我們定義了一個 logProperty 函數,來跟蹤用戶對屬性的操作,當代碼成功運行后,在控制臺會輸出以下結果:
Set: name => semlinker Set: name => kakuqo
9.2.3 方法裝飾器
方法裝飾器聲明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 方法名
descriptor: TypePropertyDescript - 屬性描述符
廢話不多說,直接上例子:
function LogOutput(tarage: Function, key: string, descriptor: any) { let originalMethod = descriptor.value; let newMethod = function(...args: any[]): any { let result: any = originalMethod.apply(this, args); if(!this.loggedOutput) { this.loggedOutput = new Array<any>();
} this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
}); return result;
};
descriptor.value = newMethod;
} class Calculator { @LogOutput double (num: number): number { return num * 2;
}
} let calc = new Calculator();
calc.double(11); // console ouput: [{method: "double", output: 22, ...}] console.log(calc.loggedOutput);
9.2.4 參數裝飾器
參數裝飾器聲明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 方法名
parameterIndex: number - 方法中參數的索引值
function Log(target: Function, key: string, parameterIndex: number) { let functionLogged = key || target.prototype.constructor.name; console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
} class Greeter {
greeting: string; constructor(@Log phrase: string) { this.greeting = phrase;
}
} // console output: The parameter in position 0 // at Greeter has been decorated
十、#XXX 私有字段
在 TypeScript 3.8 版本就開始支持 ECMAScript 私有字段,使用方式如下:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
} let semlinker = new Person("Semlinker");
semlinker.#name; // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.
與常規屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規則:
私有字段以 # 字符開頭,有時我們稱之為私有名稱;
每個私有字段名稱都唯一地限定于其包含的類;
不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private);
私有字段不能在包含的類之外訪問,甚至不能被檢測到。
10.1 私有字段與 private 的區別
說到這里使用 # 定義的私有字段與 private 修飾符定義字段有什么區別呢?現在我們先來看一個 private 的示例:
class Person { constructor(private name: string){}
} let person = new Person("Semlinker"); console.log(person.name);
在上面代碼中,我們創建了一個 Person 類,該類中使用 private 修飾符定義了一個私有屬性 name,接著使用該類創建一個 person 對象,然后通過 person.name 來訪問 person 對象的私有屬性,這時 TypeScript 編譯器會提示以下異常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解決這個異常呢?當然你可以使用類型斷言把 person 轉為 any 類型:
console.log((person as any).name);
通過這種方式雖然解決了 TypeScript 編譯器的異常提示,但是在運行時我們還是可以訪問到 Person 類內部的私有屬性,為什么會這樣呢?我們來看一下編譯生成的 ES5 代碼,也許你就知道答案了:
var Person = /** @class */ (function () { function Person(name) { this.name = name;
} return Person;
}()); var person = new Person("Semlinker"); console.log(person.name);
這時相信有些小伙伴會好奇,在 TypeScript 3.8 以上版本通過 # 號定義的私有字段編譯后會生成什么代碼:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
}
以上代碼目標設置為 ES2015,會編譯生成以下代碼:
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value); return value;
}; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance");
} return privateMap.get(receiver);
}; var _name; class Person { constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
通過觀察上述代碼,使用 # 號定義的 ECMAScript 私有字段,會通過 WeakMap 對象來存儲,同時編譯器會生成 __classPrivateFieldSet 和 __classPrivateFieldGet 這兩個方法用于設置值和獲取值。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務https://github.com/krasimir/l...
如果你必須在同一個瀏覽器中從一個標簽頁發送消息到另一個標簽頁,你不必用艱難的方式。Local storage bridge在這里讓任務變得更簡單。
基本使用:
// 發送 lsbridge.send(‘app.message.error’, { error: ‘Out of memory’ });
// 監聽 lsbridge.subscribe(‘app.message.error’, function(data) { console.log(data); // { error: ‘Out of memory’ } });
Basil.js統一了session、localStorage和cookie,為你提供了一種處理數據的直接方法。
基本使用:
let basil = new Basil(options);
basil.set(‘name’, ‘Amy’);
basil.get(‘name’);
basil.remove(‘name’);
basil.reset();
https://github.com/marcuswest...
Store.js像其他東西一樣處理數據存儲。但還有更多的功能,它的一個高級特性是讓你更深入地訪問瀏覽器支持。
基本使用:
store.set(‘book’, { title: ‘JavaScript’ }); // Store a book store.get(‘book’);
// Get stored book store.remove(‘book’); // Remove stored book store.clearAll(); // Clear all keys
https://github.com/pamelafox/...
它與localStorage API類似。事實上,它是localStorage的一個封裝器,并使用HTML5模擬memcaches函數。在上面的文檔中發現更多的功能。
基本使用:
lscache.set(‘name’, ‘Amy’, 5); // 數據將在5分鐘后過期 lscache.get(‘name’);
Lockr建立在localStorage API之上。它提供了一些有用的方法來更輕松地處理本地數據。
是什么讓你要使用此庫而不是localStorage API?
好吧,localStorage API僅允許你存儲字符串。如果要存儲數字,則需要先將該數字轉換為字符串。在Lockr中不會發生這種情況,因為Lockr允許你存儲更多的數據類型甚至對象。
基本使用:
Lockr.set(‘name’, ‘Amy’);
Lockr.set(‘age’, 28);
Lockr.set(‘books’, [{title: ‘JavaScript’, price: 11.0}, {title: ‘Python’, price: 9.0}]);
https://github.com/arokor/barn
Barn在localStorage之上提供了一個類似Redis的API。如果持久性很重要,那么你將需要這個庫來保持數據狀態,以防發生錯誤。
基本使用:
let barn = new Barn(localStorage); // 原始類型 barn.set(‘name’, ‘Amy’); let name = barn.get(‘name’);
// Amy // List barn.lpush(‘names’, ‘Amy’);
barn.lpush(‘names’, ‘James’); let name1 = barn.rpop(‘names’); // Amy let name2 = barn.rpop(‘names’);
// James
https://github.com/localForag...
這個簡單而快速的庫將通過IndexedDB或WebSQL使用異步存儲來改善Web的脫機體驗。它類似于localStorage,但具有回調功能。
基本使用:
localforage.setItem(‘name’, ‘Amy’, function(error, value) { // Do something });
localforage.getItem(‘name’, function(error, value) { if (error) { console.log(‘an error occurs’);
} else { // Do something with the value }
});
很神奇的是它提供中文文檔
https://github.com/jas-/crypt.io
crypt.io使用標準JavaScript加密庫實現安全的瀏覽器存儲。使用crypto.io時,有三個存儲選項:sessionStorage,localStorage或cookie。
基本使用:
let storage = crypto; let book = { title: ‘JavaScript’, price: 13 };
storage.set(‘book’, book, function(error, results) { if (error) { throw error;
} // Do something });
storage.get(‘book’, function(error, results) { if (error) { throw error;
} // Do something });
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
在開始正文前,我們先把本文涉及到的一些內容提前定個基調。
Promise 中只有涉及到狀態變更后才需要被執行的回調才算是微任務,比如說 then
、 catch
、finally
,其他所有的代碼執行都是宏任務(同步執行)。
上圖中藍色為同步執行,黃色為異步執行(丟到微任務隊列中)。
這個問題我們根據 ecma 規范來看:
[[PromiseFulfillReactions]]
和 [[PromiseRejectReactions]]
中。如果你看過手寫 Promise 的代碼的話,應該能發現有兩個數組存儲這些回調函數。
了解完以上知識后,正片開始。
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
})
.then(() => { console.log("then2");
});
以上代碼大家應該都能得出正確的答案:then1 → then1-1 → then2
。
雖然 then
是同步執行,并且狀態也已經變更。但這并不代表每次遇到 then
時我們都需要把它的回調丟入微任務隊列中,而是等待 then
的回調執行完畢后再根據情況執行對應操作。
基于此,我們可以得出第一個結論:鏈式調用中,只有前一個 then
的回調執行完畢后,跟著的 then
中的回調才會被加入至微任務隊列。
大家都知道了 Promise resolve
后,跟著的 then
中的回調會馬上進入微任務隊列。
那么以下代碼你認為的輸出會是什么?
let p = Promise.resolve();
p.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then1-2");
});
p.then(() => { console.log("then2");
});
按照一開始的認知我們不難得出 then2
會在 then1-1
后輸出,但是實際情況卻是相反的。
基于此我們得出第二個結論:每個鏈式調用的開端會首先依次進入微任務隊列。
接下來我們換個寫法:
let p = Promise.resolve().then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then2");
});
p.then(() => { console.log("then3");
});
上述代碼其實有個陷阱,then
每次都會返回一個新的 Promise,此時的 p
已經不是 Promise.resolve()
生成的,而是最后一個 then
生成的,因此 then3
應該是在 then2
后打印出來的。
順便我們也可以把之前得出的結論優化為:同一個 Promise 的每個鏈式調用的開端會首先依次進入微任務隊列。
以下大家可以猜猜 then1-2
會在何時打印出來?
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return 1;
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
這題肯定是簡單的,記住第一個結論就能得出答案,以下是解析:
resolve
后第一個 then
的回調進入微任務隊列并執行,打印 then1
resolve
后內部第一個 then
的回調進入微任務隊列,此時外部第一個 then
的回調全部執行完畢,需要將外部的第二個 then
回調也插入微任務隊列。
then1-1
和 then2
,然后分別再將之后 then
中的回調插入微任務隊列
then1-2
和 then3
,之后的內容就不一一說明了
接下來我們把 return 1
修改一下,結果可就大不相同啦:
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return Promise.resolve();
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
當我們 return Promise.resolve()
時,你猜猜 then1-2
會何時打印了?
答案是最后一個才被打印出來。
為什么在 then
中分別 return
不同的東西,微任務的執行順序竟有如此大的變化?以下是筆者的解析。
PS:then
返回一個新的 Promise,并且會用這個 Promise 去 resolve
返回值,這個概念需要大家先了解一下。
根據規范 2.3.2,如果 resolve
了一個 Promise,需要為其加上一個 then
并 resolve
。
if (x instanceof MyPromise) { if (x.currentState === PENDING) {
} else {
x.then(resolve, reject);
} return;
}
上述代碼節選自手寫 Promise 實現。
那么根據 A+ 規范來說,如果我們在 then
中返回了 Promise.resolve
的話會多入隊一次微任務,但是這個結論還是與實際不符的,因此我們還需要尋找其他權威的文檔。
根據規范 25.6.1.3.2,當 Promise resolve
了一個 Promise 時,會產生一個NewPromiseResolveThenableJob,這是屬于 Promise Jobs 中的一種,也就是微任務。
This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.
并且該 Jobs 還會調用一次 then
函數來 resolve Promise
,這也就又生成了一次微任務。
這就是為什么會觸發兩次微任務的來源。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
在進行小程序項目啟動,進行技術選型的時候,對市場上多個小程序框架進行了考慮:
團隊成員mpvue、wepy、uni-app都有實際的項目經驗,且根據Github上的star數還有issue,最后決定回到到使用原生開發。
雖然框架有些很成熟,有工程化和跨端的解決方案,也有實際的上線項目,但考慮到后續一些支撐性的問題(維護,文檔,坑等),在github上看了issue,有些已經沒在維護了。
想著讓項目持續迭代,不受第三方框架限制,保持穩健,最后決定使用原生,跟著官方的迭代升級,自己維護,引入前端工程化的思想,提高繁瑣的流程以及開發效率。
基于Webpack4.x,自定義Webpack配置
scss編譯為wxss:定義全局變量,使用公共的樣式文件,提高css開發效率和可維護性;
自動壓縮圖片資源 : 小程序對包大小有限制,壓縮圖片大小可以減少空間,加快頁面加載;普通的圖片壓縮需要將圖片上傳到在線圖片壓縮網站,壓縮完再保存下來,效率比較低?,F在執行命令就可以自動壓縮圖片。
代碼規范
eslint: 能在js運行前就識別一些基礎的語法錯誤,減少不必要的小問題,提高調試效率;
husky、line-staged、prettier: 統一團隊代碼規范: 當執行代碼提交到git倉庫時,會將已改動文件的代碼格式化統一規范的代碼風格;
命令行創建頁面和組件模板
引入jest單元測試
app -> 小程序程序的入口,使用微信開發者工具制定app目錄cli -> 生pages和components的模板腳手架img ->
圖片資源原文件.eslintignore.eslintrc.js.gitignore(忽略wxss的提交,多人和做改動,容易有沖突,將scss文件傳到服務器就好了).prettierrc.js(代碼格式化風格配置)babel.config.jsjest.config.js(單元測試配置文件)webpack.compress.js(指定入口圖片資源文件,將圖片壓縮編譯到小程序的資源目錄)webpack.config.js -> (工程化入口文件,指定入口scss文件,監聽文件變化,自動將scss編譯為wxss)
. 安裝依賴 npm install 或 yarn install. 編譯scss
npm run dev. 壓縮圖片 npm run img. 單元測試 npm run test(生成測試報告) npm run test:watch(監聽測試文件改動—開發環境下使用)
執行 npm run dev
執行 npm run img
將圖片壓縮到app/assets/img目錄下,一張7k的圖片變成5k,肉眼看不出有什么差別。
執行 npm run create
終端會提示選擇頁面還是組件,選擇頁面,按Enter鍵,輸入頁面的名稱,會自動將4個文件創建到app/pages/xxx下。
執行 npm run create
終端會提示選擇頁面還是組件,選擇組件,按Enter鍵,輸入組件的名稱,會自動將4個文件創建到app/components/xxx下。
執行 npm run test 生成測試報告執行 npm run test:watch 監聽測試文件,方便開發使用
工程化的初衷就是為了減少重復性的操作,提高編碼的效率和樂趣。
JavaScript是弱類型語言,好處是靈活,壞處是太靈活(多人協作,維護別人寫的代碼就是很痛苦了)。
項目最主要的是穩健,可高度自定義拓展,不拘束于版本和地上那方,特別多人協作的團隊,工程化能給團隊帶來更多的收益,后續也會考慮將TypeScript等其他好的方案引入項目。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
import 導入模塊、export 導出模塊
可以直接在任何變量或者函數前面加上一個 export 關鍵字,就可以將它導出。
在一個文件中:
export const sqrt = Math.sqrt; export function square(x) { return x * x; } export function diag(x, y) { return sqrt(square(x) + square(y)); }
import { square, diag } from 'lib'; console.log(square(11)); // 121 console.log(diag(4, 3));
總結
//mod.js // 第一種模塊導出的書寫方式(一個個的導出) // 導出普通值 export let a = 12; export let b = 5; // 導出json export let json = { a, b }; // 導出函數 export let show = function(){ return 'welcome'; }; // 導出類 export class Person{ constructor(){ this.name = 'jam'; } showName(){ return this.name; } } //index.js //導出模塊如果用default了,引入的時候直接用,若沒有用default,引入的時候可以用{}的形式 // 導入模塊的方式 import { a, b, json, show, Person } from './mod.js'; console.log(a); // 12 console.log(b); // 5 console.log(json.a); // 12 console.log(json.b); // 5 console.log(show()); // welcome console.log(new Person().showName()); // jam //mod1.js // 第二種模塊導出的書寫方式 let a = 12; let b = 5; let c = 10; export { a, b, c as cc // as是別名,使用的時候只能用別名,特別注意下 }; //index1.js // 導入模塊的方式 import { a, b, cc // cc是導出的,as別名 } from './mod1.js'; console.log(a); // 12 console.log(b); // 5 console.log(cc); // 10 //mod2.js // 第三種模塊導出的書寫方式 ---> default // default方式的優點,import無需知道變量名,就可以直接使用,如下 // 每個模塊只允許一個默認出口 var name = 'jam'; var age = '28'; export default { name, age, default(){ console.log('welcome to es6 module of default...'); }, getName(){ return 'bb'; }, getAge(){ return 2; } }; //index2.js // 導入模塊的方式 import mainAttr from './mod2.js'; var str = ' '; // 直接調用 console.log(`我的英文名是:${mainAttr.name}我的年齡是${mainAttr.age}`); mainAttr.default(); // welcome to es6 module of default... console.log(mainAttr.getName()); // bb console.log(mainAttr.getAge()); // 2 //mod3.js var name = 'jam'; var age = '28'; export function getName(){ return name; }; export function getAge(){ return age; }; //index3.js // 導入模塊的方式 import * as fn from './mod3.js'; // 直接調用 console.log(fn.getName()); //
為了實現頁面之間的通訊,或者數據交換,我們要實現一個頁面到另一個頁面的傳參,可以通過點擊跳轉的時候進行頁面之間的傳值。
<template>
<view>
<navigator url="../a/a?id=1" hover-class="none">
<view>跳轉到A頁面</view>
</navigator>
<navigator url="../b/b?id=2" hover-class="none">
<view>跳轉到B頁面</view>
</navigator>
<navigator url="../c/c?id=3" hover-class="none">
<view>跳轉到C頁面</view>
</navigator>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
},
onLoad: function (option) {
//獲得上一個頁面傳過來的id
var pageid = option.id;
console.log(pageid);
}
}
</script>
Author:TANKING
Web:http://www.likeyun.cn/
Date:2020-8-13
WeChat:face6009
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
目前除了微信小程序,還有支付寶小程序,百度小程序,字節跳動小程序、京東小程序等,各大流量平臺都希望借助小程序的服務連接能力,為企業和用戶提供更好的服務。企業在小程序賽道上的戰略布局,可以借助平臺的流量,獲得更多的用戶和變現。
有些人會認為小程序用戶增長是運營的事情,與產品和開發無關,實則不然。對于全棧工程師來說,他們不僅能做好小程序,還能玩溜小程序,真正全方位、無死角實現用戶增長最大化。
小曉云近期采訪了幾位開發者,他們的小程序有些已經突破百萬用戶,在自己的領域中獲得持續性的發展。開發者圍繞用戶增長中的拉新、留存、轉化等維度,為我們提供了關于產品設計和開發的建議,希望以下的經驗對你們都有幫助。
小程序的特點是「即搜即用,用完即走」,更適合那些輕量級的工具型應用。用戶來得快去得也快,則更需要簡潔、輕便的設計定位。UI 設計師和前端開發者平常可以通過知曉商店等小程序商店,多參考同類型優秀小程序的設計與交付,以此優化自己的小程序。雖然小程序視覺和交互并不會帶來用戶裂變式增長,但這個是留住用戶最基本的要求了。
今年微信小程序從模板消息升級為訂閱消息,這是一個幫助小程序實現用戶回流重要能力了。對于開發者來說,重點思考:如何合理的向用戶直觀傳達訂閱消息、在何處彈出訂閱消息。開發者在接入訂閱消息能力時,應該選擇適合業務場景的模板,并進行文案的引導,同時可以嘗試,發起一次訂閱時同時訂閱多個模版,讓用戶一次性獲得更多消息,提高訂閱率。新用戶粘性較低,可以借助訂閱消息發送獎勵通知等,召回用戶。
我們原先是做體育領域的資訊和商城 App,微信小程序發布后,龐大的微信流量給了我們一個拓展用戶的新機會,于是在 2017 年與知曉云結緣,并相伴 3 年了。用戶增長其實不是簡單的一兩句話可以總結,所以我先分享其中一點。體育領域的資訊,能吸引和留住用戶的,主要靠資訊的時效性、賽事活動、體育明星的加持,因此我們曾組織體育明星的投票活動,粉絲樂于參與,同時也提高了認同感和優越感。投票功能設置的門檻很低,但我們增加了很多排行榜的文案引導,加強粉絲的緊迫感,同時也增加了很多分享文案引導,讓粉絲主動分享。這個是針對體育領域上很成功的增長案例,當然也可以應用于其它追星平臺等。
精細化運營的核心就是數據驅動,明確關鍵指標,并且通過數據分析的方式進行評估,然后不斷優化。數據埋點的缺點是開發成本高,所以我們是基于無埋點,一次性集成 SDK,采集頁面訪問、點擊行為、用戶特征等全量數據。
我是做 QQ 小程序的, QQ 小程序的用戶對于社交、戀愛等偏娛樂的場景更感興趣,針對性提供頭像制作、起網名、小游戲等服務有比較大的市場。所以可以結合平臺用戶的特點和喜好,開發相關服務的小程序,更有利于小程序的發展和變現。
以上內容均來自知曉云開發者的經驗分享,如果你對哪一個內容感興趣,可以在文末或者小曉云微信上留言,對于大家感興趣的內容,我們將再次邀請開發者進行更全面更完整的分享。
知曉云成立三年以來,通過提供不斷更新的開發工具,幫助開發者提高開發效率,輕松完成優秀的作品。但我們服務不止于快、省,還要在增長與變現上賦能開發者/企業。
在線可視化操作加上業務系統輕松集成,可以一鍵推送全平臺的訂閱消息推送服務,輕松觸達億萬用戶。
支持各類高實時性的業務場景,以簡便的開發方式、更高的時效性實現在云端和客戶端來的數據實時同步。通過實時數據庫實現的即時聊天室、投票、直播間送禮和彈幕、小游戲等互動功能,提升用戶留存和轉化。
一鍵生成可視化的運營管理后臺,User Dash API 和開箱即用的前端組件庫,開發者可以快速編寫一套獨立的運營后臺,并支持一鍵部署至知曉云服務器,是開發者的利器,也是運營者的福音。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
微信小商店已經正式上線,對企業、個體和個人三種開店類型全量開放。微信小商店可以幫助商家免開發、零成本、一鍵生成賣貨小程序。微信小商店團隊將負責商品發布、訂單管理、交易結算、物流售后、直播帶貨等技術和服務流程。
微信小商店個人開店非常簡單,3秒搞定,毫不夸張,堪稱模板商終結者。個人開店僅需身份認證即可,綁定銀行卡可以提現,1個微信號僅支持開通1個個人主體的小商店。
企業、個體工商戶需要上傳營業執照、經營者信息、結算銀行賬戶信息等基礎信息,1個微信號可以開通3個“企業和個體”主體的小商店。
當前微信小商店現階段支持售賣的商品類目超過1500個,主要包括:寵物生活、家用電器、手機、通訊、數碼、電腦、辦公、服飾內衣等,后續可售品類會增多。
小商店助手
在 小商店助手
里面還能查看店鋪數據
、在售商品
、新增商品
、待付款商品
、訂單管理
、客服管理
、店鋪設置
等,功能非常強大!
上架新商品也是非常簡單快捷,直接上傳商品圖片,加上標題和一些描述信息就可以。
而且不論是開店審核還是商品上架審核,都非常迅速,作者嘗試了幾次都在一分鐘左右就審核完了!
需要注意的一點是微信會收 0.6%
的交易金額提成哦,畢竟此路他開此樹他栽樹嘛~