TypeScript can be thought of as a superset of JavaScript, meaning that it covers all JavaScript functionality and has its own unique syntax on top of it. Recently, a new project started TS pit trekking tour. Now I share some tricks that can be used for reference.
Why choose TS
As a static strongly typed compiled language produced by Giant Hard Company, the language has appeared for several years, I believe that under the maintenance of the community, it is already a very stable language. As we all know, JavaScript is a dynamic weakly typed interpreted scripting language, which brings a lot of convenience. We can change variable types randomly during code execution to achieve the desired purpose. But at the same time, it can be a double-edged sword. When you have a huge project in front of you, with incredibly complex logic, it’s hard to see what type a variable is and what it’s supposed to do in the code.
Statically strongly typed compilation offers a number of benefits, one of the most important of which is that it prevents developers from being sloppy:
The graph shows the top ten exceptions from the thousands of items counted by RollBar
As you can see, there are more exceptions due to type mismatches and null variables than you dare admit. Such as
Another benefit of statically compiled types is function signatures. Again, as mentioned above, because it is a dynamic scripting language, it is difficult for an editor to correctly tell you during development what parameters to pass and what type of return value to return when a function is called.
In TS, for a function, you first need to define the types of all the arguments and the types of the return values. When this function is called, we can clearly see the effect of this function:
This is the most basic level, can make the program more stable two features, of course, there are more features in the TS: TypeScript | faced
TypeScript in Node
There are plenty of examples on the TS website, and one of them is the Express version, which has been modified for a KOA project.
Environment depends on
Before using TS, you need to prepare these things:
- VS Code, also produced by Giant Hardware, is itself developed by TS, so the editor is currently the most supported for TS
- Node.js version 8.11 or later is recommended
npm i -g typescript
, global install TS, compile the TSC command used herenpm i -g nodemon
, install Nodemon globally, and automatically refresh the server program after TSC compilation
- The official manual
- Official Express Example
And some of the core dependencies used in the project:
reflect-metadata
: a base package that a large number of decorator packages rely on to inject datarouting-controllers
: KoA-Router development using decoratorssequelize
Abstract database operationssequelize-typescript
: Decorator version of the above plug-in, used when defining entities
The project structure
First, release the current project structure:
..Bass Exercises ── Heavy Metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── Heavy metal Exercises ── ─ Heavy metal Exercises ── ─ Heavy metal Exercises ── ── ─ Heavy metal Exercises ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ Config │ ├── Controllers │ ├─ entity │ ├── Models │ ├─ Public │ ├── Middleware Types │ ├─ ├─ trash ├─ tsci.txtCopy the code
SRC is the main development directory, where all the TS code is stored. After compiling, a dist folder, the same as SRC, is generated. This folder is the actual node engine code. Under SRC, the main code is divided into the following structure (adding and deleting according to the actual situation of your project) :
# | folder | desc |
---|---|---|
1 | controllers |
Used to process interface requestsapps ,routes Folder. |
2 | middleware |
Store a variety of middleware, global or custom middleware |
3 | config |
Location of various configuration items, including ports,log Paths, various blah blah constant definitions. |
4 | entity |
This is where all entity definitions are stored (using sequelize for database operations). |
5 | models |
Use fromentity The entity insequelize To complete the initialization operation, and willsequelize Object thrown. |
6 | utils |
Store a variety of common functions distilled from daily development |
7 | types |
Store a variety of customized compound type definition, a variety of structure, attribute, method return value definition (currently including the commonly used Promise version redis and Qconf) |
controllers
Controllers handle only the logic, adding, deleting, and checking data by manipulating model objects instead of the database
Given that most of the company’s Node project has been upgraded to Node 8.11, it is only natural that we should try out the new syntax. That is to say we will abandon Generator and embrace async/await.
As anyone who has written interfaces with Koa or Express knows, when a project gets big, it actually generates a lot of repetitive nonlogical code:
router.get('/'.ctx= > {})
router.get('/page1'.ctx= > {})
router.get('/page2'.ctx= > {})
router.get('/page3'.ctx= > {})
router.get('/pageN'.ctx= > {})
Copy the code
In each route listening, a lot of repetitive work is done:
router.get('/'.ctx= > {
let uid = Number(ctx.cookies.get('uid'))
let device = ctx.headers['device'] | |'ios'
let { tel, name } = ctx.query
})
Copy the code
The header of almost every route is used to fetch parameters, which can come from headers, bodies, or even cookies and queries.
So, we made a major change to the original koA usage, and used routing-controllers with a lot of application decorators to help us deal with most of the nonlogical code.
Original router definition:
module.exports = function (router) {
router.get('/'.function* (next) {
let uid = Number(this.cookies.get('uid'))
let device = this.headers['device']
this.body = {
code: 200}})}Copy the code
Uses TypeScript and decorator definitions:
@Controller
export default class {
@Get('/')
async index (
@CookieParam('uid') uid: number.@HeaderParam('device') device: string
) {
return {
code: 200}}}Copy the code
In order to make the interface easier to retrieve and clearer, we abandoned the original function of BD-Router (according to the file path as the interface path, the file path in TS only used for file stratification). Directly declare the corresponding interface to listen in the file under controllers.
middleware
If it is global Middleware directly on the class add @ Middleware decorator, and set the type: ‘after | before’. For specific middleware, create a normal class and specify @usebefore / @useAfter on the controller object you want to use (either class or method).
All middleware needs to inherit and implement the corresponding MiddlewareInterface interfaceuse
methods
// middleware/xxx.ts
import {ExpressMiddlewareInterface} from ".. /.. /src/driver/express/ExpressMiddlewareInterface"
export class CompressionMiddleware implements KoaMiddlewareInterface {
use(request: any, response: any, next? :Function) :any {
console.log("hello compression ...")
next()
}
}
// controllers/xxx.ts
@UseBefore(CompressionMiddleware)
export default class{}Copy the code
entity
The file only defines the data model and does nothing logical
Again using the sequelize+ decorator approach, entity is simply used to create a data model that communicates with the database.
import { Model, Table, Column } from 'sequelize-typescript'
@Table({
tableName: 'user_info_test'
})
export default class UserInfo extends Model<UserInfo> {
@Column({
comment: 'on the ID',
autoIncrement: true,
primaryKey: true
})
uid: number
@Column({
comment: 'name'
})
name: string
@Column({
comment: 'age',
defaultValue: 0
})
age: number
@Column({
comment: 'gender'
})
gender: number
}
Copy the code
Sequelize also requires the corresponding database address, account, password, and database information to establish a connection. Therefore, it is recommended that sequelize store all entities of the same database in the same directory so that Sequelize can load the corresponding model. Add a list of keys to hold the entity. In this way, all entities under the path can be dynamically imported when the database link is established and the data model is loaded:
// config.ts
export const config = {
// ...
mysql1: {
// ... config
+ entity: 'entity1' // Add a column of keys to identify the entity
},
mysql2: {
// ... config
+ entity: 'entity2' // Add a column of keys to identify the entity
}
// ...
}
// utils/mysql.ts
new Sequelize({
// ...
modelPath: [path.reolve(__dirname, `.. /entity/${config.mysql1.entity}`)]
// ...
})
Copy the code
model
The model is positioned to create abstracted database objects based on their corresponding entities, and because of sequelize, the files in that directory are very concise. Basically, you initialize the Sequelize object and throw it when the model is loaded.
export default new Sequelize({
host: '127.0.0.1',
database: 'database',
username: 'user',
password: 'password',
dialect: 'mysql'.// Or some other database
modelPaths: [path.resolve(__dirname, `.. /entity/${configs.mysql1.entity}`)].// Load our entity
pool: { // Some configuration related to the connection pool
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
operatorsAliases: false,
logging: true // true prints the corresponding SQL command for each sequelize operation on the console
})
Copy the code
utils
All the public functions, I put them here. It is recommended to compile the corresponding index file (index.ts) in the following format:
// utils/get-uid.ts
export default function () :number {
return 123
}
// utils/number-comma.ts
export default function() :string {
return '1234'
}
// utils/index.ts
export {default as getUid} from './get-uid'
export {default as numberComma} from './number-comma'
Copy the code
Every time a new util is added, the corresponding index is added to index. This has the advantage of importing all the desired utils in a single line:
import {getUid, numberComma} from './utils'
Copy the code
configs
Configs stores various configuration information, including third-party interface urls, database configurations, and log paths. Static data for various Balabala. If there are many configuration files, it is recommended to split them into multiple files and compile the index file as utils does.
types
This is where all the custom type definitions are stored, some of which are not provided by the open source community, but we use third-party plug-ins, which need to be defined here. Generally, there are some common packages, but some of them do not support TS. For example, we use a node-qconf:
// types/node-qconf.d.ts
export function getConf(path: string) :string | null
export function getBatchKeys(path: string) :string[] | null
export function getBatchConf(path: string) :string | null
export function getAllHost(path: string) :string[] | null
export function getHost(path: string) :string | null Copy the code
All files with the suffix.d. types can be referenced directly, without worrying about relative paths (which other normal models require, which can be awkward).
Some problems with TS currently used
The e only awkward problem we have is that the reference file path must be completely written.
import module from '.. /.. /.. /.. /f**k-module'
Copy the code
summary
When I first tried TypeScript, I fell in love with it. There are some minor issues, but I can overcome them. Using a statically strongly typed compilation language can eliminate many bugs during development.
A simple example based on the above description: code repository
Have fun and feel free to harass any TS related questions. NPM loves U.