preface
As a weakly typed language, JS is often “discriminated” by Java, C# and other established programming languages. In addition, JS can only run on the browser at the beginning of its birth, and is nicknamed as “toy language”. With the early advent of Node.js, JS made inroads into the back-end realm. Now TypeScript has breathed new life into the language, essentially adding optional static typing and class-based object-oriented programming as a superset of JavaScript that fits nicely with the most popular front-end framework, React. At the moment it’s all the rage.
In 2019, we need to fully grasp TypeScript and put it into practice.
This project takes TodoList with clear functions as the entry point, combines React bucket and Antd to create a front-end application with strong code robustness and simple user interface, and builds a high-maintainability back-end service with Koa2 + MongoDB as the core.
TodoList is a best practice for full stack applications. You can try it yourself.
Online access address
Technology stack
- The front end
- TypeScript makes JS a strongly typed language
- React (the most popular front-end framework of the moment)
- Axios (processing HTTP requests)
- Ant-design (UI Framework)
- React-router (handles page routing)
- Redux (Data State Management)
- Redux-saga (handling asynchronous actions)
- The back-end
- Koa2 (Next generation Web development framework based on Node.js platform)
- MongoDB (non-relational database)
The function point
- RESTful interface design
- HTTP request encapsulation, error handling
- Componentization, code layering
- User login and registration
- Todo keyword query
- Todo content modification
- Todo status changed
- Todo record deleted
Practice analysis
TypeScript
TS basically gives us the ability to type JS variables. It also introduces interfaces, generics, enumerations, classes, decorators, namespaces, and more.
let a: number = 1; // int a = 1;
let b: string = "Hello"; // string b = 'Hello'
let arr: number[] = [1.2.3]; // int arr[] = {1,2,3};
Copy the code
TS can constrain our parameters, variable types, and interface types to avoid unnecessary errors during development.
An interface is exported, using /interface/ userstate. ts as an example
export interfaceUserState { user_id? :string; / /? On behalf of the optionalusername? :string; err_msg? :string;
}
Copy the code
User inherits UserState interface, which will have attribute derivation, while in JS, we need to input user.err_msg ourselves, which is tedious and error-prone.
In React, we mainly use stateless function and stateful class components to build applications, including functions passing, function passing, and class inheritance. Our code robustness went up a notch.
Redux state management
At present, State management is an indispensable part of building single-page applications. Simple applications can use State within components easily and quickly. However, as the complexity of applications increases, data will be scattered among different components, and component communication will become extremely complex. It follows three principles:
- Component data comes from Store and flows in one direction
- State can only be changed by triggering an action, which is globally unique by defining actionTypes
- Reducer is a pure function
Because Reducer can only be a pure function (simply speaking, the return result of a function depends only on its parameters and has no side effects during execution, we call this function a pure function.) While in the Fetch scenario, the Action needs to initiate an asynchronous request, which has side effects. Therefore, we use redux-saga to process the asynchronous Action. After processing, the successful synchronous Action is returned and triggered, which is a pure function, and finally changes the store data.
In the FETCH_TODO example, the data flow is as follows:
Interface design
Due to the use of front-end and back-end separation of development, we use the convention interface for data exchange, and the most popular is RESTful interface, which has the following key points:
- According to the request purpose, set the corresponding HTTP Method, for example, GET corresponding to Read resources, PUT corresponding to Update resources, POST corresponding to Created resources, DELETE corresponding to DELETE resources, corresponding to database CRUD operations
- The verb indicates the request mode, and the noun indicates the data source. The plural form is usually used, for example, GET/users/2 to obtain the user whose ID is 2
- Return the corresponding HTTP status code. Common examples are:
200 OK
Request successful,201 CREATED
Created successfully,202 ACCEPTED
Update successful,204 NO CONTENT
Deleted successfully,401 UNAUTHORIZED
Unauthorized,403 FORBIDDEN
Access is prohibited,404 NOT FOUND
Resources don’t exist,500 INTERNAL SERVER ERROR
An internal error occurred on the server
Using Todo routing as an example, we can design the following interface
const todoRouter = new Router({
prefix: "/api/todos"}); todoRouter .get("/:userId/all".async (ctx: Context) => {}) // Get all toDos
.post("/search".async (ctx: Context) => {}) // Keyword search
.put("/status".async (ctx: Context) => {}) // Change the status
.put("/content".async (ctx: Context) => {}) // Modify the content
.post("/".async (ctx: Context) => {}) / / add Todo
.delete("/:todoId".async (ctx: Context) => {}); / / delete Todo
Copy the code
Layer code
First look at the server directory:
|--server
|--db
|--interface
|--routes
|--service
|--utils
|--app.ts
|--config.ts
Copy the code
We focus on db, service and routes.
db
Establish data Model (Model), equivalent to MySQL table building linkservice
Call the data model to process the business logic of the database, CURD the database, and return the processed dataroutes
Call the method in the service to process the routing request and set the request response
Those who have learned Java know that an interface can only be invoked at the Controller layer through the Domain layer, DAO layer and Service layer. Our project is similar to this idea. Better logical layering can not only improve the maintenance of the project, but also reduce the degree of coupling. This is especially important in large projects.
Error handling
Using service/user as an example, we define the userService class to handle the business logic of user, where addUser is the method to be invoked when the user is registered.
export default class UserService {
public async addUser(usr: string, psd: string) {
try {
const user = new User({
usr,
psd,
});
// If usr is duplicated, mongodb throws an exception for duplicate key
return await user.save();
} catch (error) {
throw new Error("User name already exists ( ̄o ̄).zz"); }}}Copy the code
Because we set the usr field to be unique, an exception will be thrown when the user registers by entering a user name that has already been registered. At this point, we catch and throw an exception to the route calling this method, and the routing layer catches the error and returns an HTTP response with the existing user name. This is a typical error handling process.
userRouter.post("/".async (ctx: Context) => {
const payload = ctx.request.body as IPayload;
const { username, password } = payload;
try {
const data = await userService.addUser(username, password);
if (data) {
createRes({
ctx,
statusCode: StatusCode.Created, }); }}catch (error) {
createRes({
ctx,
errorCode: 1.msg: error.message, }); }});Copy the code
A unified response
For the return result of the API call, to format the response body, we write a generic function to handle the response in /utils/response.ts.
Returns a set of messages indicating whether the call was successful. Such messages usually have a common message body style.
The common return format is a JSON response body consisting of MSG, error_code, and data:
import { Context } from "koa";
import { StatusCode } from "./enum";
interface IRes {
ctx: Context; statusCode? :number; data? :any; errorCode? :number; msg? :string;
}
const createRes = (params: IRes) = > {
params.ctx.status = params.statusCode! || StatusCode.OK;
params.ctx.body = {
error_code: params.errorCode || 0.data: params.data || null.msg: params.msg || ""}; };export default createRes;
Copy the code
When we request GET/API /todos/:userId/all, we GET all the toDos of the specified user, and return the following response body:
{
"error_code": 0."data": [{"_id": "5e9b0f1b576bd642796dd7d0"."userId": "5e9b0f08576bd642796dd7cf"."content": "Become full stack engineer ~~~"."status": false."__v": 0}]."msg": ""
}
Copy the code
Not only is this more normative, but it also makes it easier for the front end to receive requests and make better judgments or errors
TodoList: GitHub address