node + koa + ts серверное приложение для сборки

Node.js

Стартовый учебник для начинающих или изучение внешнего интерфейса node.js в качестве фона

содержание:

  1. Сборка и настройка проекта
  2. Front-end и back-end запросыheaderОсновные инструкции по настройке
  3. Запись интерфейса и обработка параметров
  4. загрузить изображение
  5. Связать базу данных и интерфейсные операции
  6. Войти Зарегистрироваться Пользовательский модуль
  7. jwt-tokenИспользование аутентификации (здесь я использую модуль, который написал сам)
  8. Добавить, удалить, изменить и проверить функцию
  9. Строительство проекта, доставка до линии

Здесь я используюtypescriptПричина его написания — очень полезная подсказка типов и трассировка кода, поэтому в чистом видеjavascriptВ проектах по программированиюtypescriptлучше всего поддерживается и читается, используется здесьvscodeэтот редактор кода

Кодовый адрес:node-koa

Давайте сначала посмотрим на структуру каталогов

public

В шаблоне хранится статическая страница api-xxx.html, такая как страница отладки внешнего интерфейса.

user.json Временная таблица для хранения данных jwt-токена

upload Временный каталог для хранения загруженных файлов

src

API Каждый интерфейсный модуль также является каталогом маршрутизации.

модули Некоторый каталог функций класса

каталог инструментов utils

Сборка и настройка проекта

1. cd projectи создатьsrcсодержание

mkdir src

2. Инициализироватьpackage.json, в него будут записываться все последующие конфигурации и команды

npm init

3. Установкаkoaи соответствующий маршрутkoa-router

npm install koa koa-router 

4. УстановкаTypeScriptВ соответствии с обнаруженным типом наконечников

npm install --save-dev @types/koa @types/koa-router 

5. Тогда естьTypeScriptПодборка горячих обновлений

npm install --save-dev typescript ts-node nodemon

Здесь будет яма (здесь используется оконная среда).Если установка не удалась или не может быть выполнена после установки, онаts-nodeиnodemonЭти две команды должны быть установлены глобально для выполнения горячего обновления: вот так

npm install -g -force ts-node nodemon

6. Настройте сноваpackage.jsonнастраивать

"scripts": {
    "start": "tsc && node dist/index.js",
    "serve": "nodemon --watch src/**/* -e ts,tsx --exec ts-node ./src/index.ts"
},

Если это не работаетnpm run serveЗатем скопируйте его вручнуюnodemon --watch src/**/* -e ts,tsx --exec ts-node ./src/index.ts

Не уверен еслиwindowПроблема в окружающей среде по-прежнемуnpmПроблема в том, что при первом создании и выполнении проекта все зависимости могут быть установлены локально иnpm run serveТоже может быть выполнено отлично, но при повторном открытии проекта возникает ошибка, и причина пока не найдена, но вышеописанными способами можно решить

7. Дополнительное промежуточное ПОkoa-bodyПромежуточное ПО используется для анализа параметров POST и загрузки изображений.

npm install koa-body

8. Установите модули базы данных и соответствующие зависимости типов

npm install mysql
npm install --save-dev @types/mysql

9. Наконец, настройте параметры кода

modules/config.tsНастройки проекта

class ModuleConfig {

    /** 端口号 */
    readonly port = 1995;

    /** 数据库配置 */
    readonly db = {
        host: "localhost",
        user: "root",
        password: "root",
        /** 数据库名 */
        database: "node_ts", // 待会创建数据库的时候就是这个名字
        /** 链接上限次数 */
        connection_limit: 10
    }

    /** 接口前缀 */
    readonly api_prefix = "/api/v1/";

    /** 上传图片存放目录 */
    readonly upload_path = "public/upload/images/";

    /** 上传图片大小限制 */
    readonly upload_img_size = 5 * 1024 * 1024;

    /**
     * 前端上传图片时约定的字段
     * @example 
     * const formData = new FormData()
     * formData.append("img", file)
     * XHR.send(formData)
     */
    readonly upload_img_name = "img";    

    /** 用户临时表 */
    readonly user_file = "public/user.json";

    /** token 长度 */
    readonly token_size = 28;

    /** token 格式错误提示文字 */
    readonly token_tip = "无效的token";
}

/** 项目配置 */
const config = new ModuleConfig();

export default config;

Front-end и back-end запросыheaderОсновные инструкции по настройке

index.tsПод файлом зависимы некоторые интерфейсыtoken, так что я поставилtokenКод судебного решения также написан здесь

jwt-tokenЯ объясню модуль позже, сначала напишите что-нибудь, не приносяtokenобработка интерфейса

import * as Koa from "koa";                     // learn: https://www.npmjs.com/package/koa
import * as koaBody from "koa-body";            // learn: http://www.ptbird.cn/koa-body.html
import config from "./modules/Config";
import router from "./api/main";
import "./api/apiUser";                         // 用户模块
import "./api/apiUpload";                       // 上传文件模块
import "./api/apiTest";                         // 基础测试模块
import "./api/apiTodo";                         // 用户列表模块
import { TheContext } from "./utils/interfaces";

const App = new Koa();

// 先统一设置请求配置 => 跨域,请求头信息...
App.use(async (ctx: TheContext, next) => {
    // /** 请求路径 */
    // const path = ctx.request.path;

    console.log("--------------------------");
    console.count("request count");
    
    ctx.set({
        "Access-Control-Allow-Origin": "*", // 指定请求域,* 就是所有域名都可访问,即跨域打开
        // "Content-Type": "application/json",
        // "Access-Control-Allow-Credentials": "true",
        // "Access-Control-Allow-Methods": "OPTIONS, GET, PUT, POST, DELETE",
        "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization",
        // "X-Powered-By": "3.2.1",
        // "Content-Security-Policy": `script-src "self"` // 只允许页面`script`引入自身域名的地址
    });

    // const hasPath = router.stack.some(item => item.path == path);
    // // 判断是否 404
    // if (path != "/" && !hasPath) {
    //     return ctx.body = "<h1 style="text-align: center; line-height: 40px; font-size: 24px; color: tomato">404:访问的页面(路径)不存在</h1>";
    // }

    // 如果前端设置了 XHR.setRequestHeader("Content-Type", "application/json")
    // ctx.set 就必须携带 "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization" 
    // 如果前端设置了 XHR.setRequestHeader("Authorization", "xxxx") 那对应的字段就是 Authorization
    // 并且这里要转换一下状态码
    // console.log(ctx.request.method);
    if (ctx.request.method === "OPTIONS") {
        ctx.response.status = 200;
    }
    
    try {
        await next();
    } catch (err) {
        ctx.response.status = err.statusCode || err.status || 500;
        ctx.response.body = {
            message: err.message
        }
    }
});

// 使用中间件处理 post 传参 和上传图片
App.use(koaBody({
    multipart: true,
    formidable: {
        maxFileSize: config.uploadImgSize
    }
}));

// 开始使用路由
App.use(router.routes())

// 默认无路由模式
// App.use((ctx, next) => {
//     ctx.body = html;
//     // console.log(ctx.response);
// });

App.on("error", (err, ctx) => {
    console.error("server error !!!!!!!!!!!!!", err, ctx);
})

App.listen(config.port, () => {
    console.log(`server is running at http://localhost:${ config.port }`);
})

// 参考项目配置连接: https://juejin.im/post/5ce25993f265da1baa1e464f
// mysql learn: https://www.jianshu.com/p/d54e055db5e0

Запустите проект, когда закончите(горячее обновление кода)

npm run watch-update

Запись интерфейса и обработка параметров

Сначала определите маршрут, а затем экспортируйте его для использования.Позже может быть несколько интерфейсов модуля, поэтому все они используются на основе этого.index.tsСлишком

api/main.tsпод файлом

import * as Router from 'koa-router';       
/** api路由模块 */
const router = new Router();
export default router;

api/apiTest.tsфайл, чтобы написать файл, который не нужно подключать к базе данныхGETиPOSTЗапрос используется в качестве теста, и параметры получены.После его написания делается фронтальный запрос.Я не буду объяснять код фронтенда, но вы можете понять его, прочитав комментарии. После написания перейдите на главную страницу, чтобы узнать, правильно ли она работает.

import * as fs from "fs";
import * as path from "path";
import router from "./main";
import utils from "../utils";
import { apiSuccess } from "../utils/apiResult";
import config from "../modules/Config";

/** 资源路径 */
const resourcePath = path.resolve(__dirname, '../../public/template');

const template = fs.readFileSync(resourcePath + "/page.html", "utf-8");

// "/*" 监听全部
router.get("/", (ctx, next) => {
    // 指定返回类型
    // ctx.response.type = "html";
    ctx.response.type = "text/html; charset=utf-8";

    const data = {
        pageTitle: "serve-root",
        jsLabel: "",
        content: `<button class="button button_green"><a href="/home">go to home<a></button>`
    }

    ctx.body = utils.replaceText(template, data);
    // console.log("根目录");

    // 路由重定向
    // ctx.redirect("/home");

    // 302 重定向到其他网站
    // ctx.status = 302;
    // ctx.redirect("https://www.baidu.com");
})

router.get("/home", (ctx, next) => {
    ctx.response.type = "text/html; charset=utf-8";

    const data = {
        pageTitle: "serve-root",
        jsLabel: "",
        content: `<h1 style="text-align: center; line-height: 40px; font-size: 24px; color: #007fff">Welcome to home</h1>`
    }

    ctx.body = utils.replaceText(template, data);
    // console.log("/home");
})

// get 请求
router.get("/getData", (ctx, next) => {
    /** 接收参数 */
    const params: object | string = ctx.query || ctx.querystring;

    console.log("/getData", params);

    ctx.body = apiSuccess({
        method: "get",
        port: config.port,
        date: utils.formatDate()
    });
})

// post 请求
router.post("/postData", (ctx, next) => {
    /** 接收参数 */
    const params: object = ctx.request.body || ctx.params;

    console.log("/postData", params);

    const result = {
        data: "请求成功"
    }

    ctx.body = apiSuccess(result, "post success")
})

загрузить изображение

загрузить на сервер

modules/api/apiUpload.tsпод файлом,

import * as fs from "fs";
import * as path from "path";
import router from "./main";
import config from "../modules/Config";
import { UploadFile } from "../utils/interfaces";
import { apiSuccess } from "../utils/apiResult";

// 上传图片
// learn: https://www.cnblogs.com/nicederen/p/10758000.html
// learn: https://blog.csdn.net/qq_24134853/article/details/81745104
router.post("/uploadImg", async (ctx, next) => {

    const file: UploadFile = ctx.request.files[config.uploadImgName] as any;
    
    let fileName: string = ctx.request.body.name || `img_${Date.now()}`;

    fileName = `${fileName}.${file.name.split(".")[1]}`;

    // 创建可读流
    const render = fs.createReadStream(file.path);
    const filePath = path.join(config.uploadPath, fileName);
    const fileDir = path.join(config.uploadPath);

    if (!fs.existsSync(fileDir)) {
        fs.mkdirSync(fileDir);
    }

    // 创建写入流
    const upStream = fs.createWriteStream(filePath);

    render.pipe(upStream);

    // console.log(fileName, file);
    
    const result = {
        image: `http://${ctx.headers.host}/${config.uploadPath}${fileName}`,
        file: `${config.uploadPath}${fileName}`
    }

    ctx.body = apiSuccess(result, "上传成功");
})

Загрузить в облако Alibaba

Сначала установите соответствующий SDK, просто прочитайте документацию здесь, код очень простой

# 模块依赖
npm install ali-oss -S
# 类型包
npm install @types/ali-oss -D

сегмент кода

import * as path from "path";
import * as OSS from "ali-oss";

import router from "./main";
import config from "../modules/Config";
import { TheContext, UploadFile } from "../utils/interfaces";
import { apiSuccess } from "../utils/apiResult";

/**
 * - [阿里云 OSS-API 文档](https://help.aliyun.com/document_detail/32068.html?spm=a2c4g.11186623.6.1074.612626fdu6LBB7)
 * - [用户管理](https://ram.console.aliyun.com/users)
 */
const client = new OSS({
    // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
    region: "oss-cn-guangzhou",
    accessKeyId: "阿里云平台生成的key",
    accessKeySecret: "阿里云平后台生成的secret",
    bucket: "指定上传的 bucket 名",
});

// 上传图片
// learn: https://www.cnblogs.com/nicederen/p/10758000.html
// learn: https://blog.csdn.net/qq_24134853/article/details/81745104
router.post("/uploadImg", async ctx => {

    const file: UploadFile = ctx.request.files[config.uploadImgName] as any;
    // console.log("file >>", file);
    
    let fileName: string = `${Math.random().toString(36).slice(2)}-${Date.now()}-${file.name}`;

    const result = await client.put(`images/${fileName}`, path.normalize(file.path));
    // console.log("上传文件结果 >>", result);

    ctx.body = apiSuccess(result, "上传成功");
})

jwt-tokenИспользование (здесь я использую модуль, написанный мной)

Идея реализации: использоватьjsпамять для чтения и записи пользовательскихtokenинформация, только при записи в память (асинхронная запись предотвращает блокировку)jsonформат для записиuser.jsonфайл, а затем прочитать последнюю запись при создании экземпляраtokenинформацию и отбрасывать устаревшие. Делайте выводы друг о друге, вы можете использовать его при написании или чтенииRedisспособ вместо чтения и записи локальногоuser.jsonТаблица, принцип тот же.

Процесс реализации:

  1. существуетpublic/Создайте новый в каталогеuser.jsonфайл как временную таблицу записей токенов

  2. затем определитеModuleJWTмодуль,userRecordЭта частная собственность являетсяtokenзаkeyОбъект для хранения информации о пользователе, эта информация имеет параметрonlineЭто означает онлайн-время, которое будет использоваться при оценке значения позже.

  3. ModuleJWTВ модуле всего 3 метода, выставленных во внешний мир:

    • setRecordУстановите запись в первый раз и вернитесь к входу по токену, и запишите вuserRecordВнутри, а потом пишем во временную таблицуuser.json.

    • updateRecordЭто метод, который будет выполняться первым каждый раз, когда делается запрос для оценки передачи внешнего интерфейса.tokenВы вuserRecordВ ней, если она есть, то судиuserRecord[token].onlineи другие операции, все вынесенные решения указывают текущийtokenнет проблем и обновитьuserRecord[token].onlineТекущее время и, наконец, запись во временную таблицу. Этот процесс может быть более сложным, но лучше посмотреть на код, чтобы понять его.

    • removeRecordЭта логика проста, прямо изuserRecordудалить текущий токен в

  4. ModuleJWTПри создании экземпляра начните сuser.jsonПрочитайте последнюю записанную информацию во временной таблице, а затем удалите устаревшиеtoken, чтобы обеспечить синхронизацию данных.

  5. Наконец, в процессе выполнения кода регулярно удаляйте устаревшие токены, чтобы снизить производительность чтения и записи и размер памяти, это зависит от кода.

modules/Jwt.tsпод файлом

import * as fs from "fs";
import config from "./Config";
import { apiSuccess } from "../utils/apiResult";
import { 
    UserRecordType, 
    UserInfoType, 
    JwtResultType, 
    TheContext,
    ApiResult
} from "../utils/interfaces";

/**
 * 自定义`jwt-token`验证模块,区别于[koa-jwt](https://www.npmjs.com/package/koa-jwt)
 * @author [Hjs](https://github.com/Hansen-hjs)
 */
class ModuleJWT {
    constructor() {
        this.init();
    }

    /** 效期(小时) */
    private maxAge = 12;

    /** 更新 & 检测时间间隔(10分钟) */
    private interval = 600000;

    /** 用户`token`纪录 */
    private userRecord: UserRecordType = {};

    /**
     * 写入文件
     * @param obj 要写入的对象
     */
    private write(obj?: UserRecordType) {
        const data = obj || this.userRecord;
        // 同步写入(貌似没必要)
        // fs.writeFileSync(config.userFile, JSON.stringify(data), { encoding: "utf8" });
        // 异步写入
        fs.writeFile(config.userFile, JSON.stringify(data), { encoding: "utf8" }, err => {
            if (err) {
                console.log(`\x1B[41m jwt-token 写入失败 \x1B[0m`, err);
            } else {
                console.log(`\x1B[42m jwt-token 写入成功 \x1B[0m`);
            }
        })
    }

    /** 从本地临时表里面初始化用户状态 */
    private init() {
        // fs.accessSync(config.userFile)
        if (!fs.existsSync(config.userFile)) {
            console.log(`\x1B[42m ${config.userFile} 不存在,开始创建该文件 \x1B[0m`);
            fs.writeFileSync(config.userFile, "{}", { encoding: "utf8" });
        }
        const userFrom = fs.readFileSync(config.userFile).toString();
        this.userRecord = userFrom ? JSON.parse(userFrom) : {};
        this.checkRecord();
        // console.log("token临时表", userFrom, this.userRecord);
    }

    /** 生成`token` */
    private getToken() {
        const getCode = (n: number): string => {
            let codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789";
            let code = "";
            for (let i = 0; i < n; i++) {
                code += codes.charAt(Math.floor(Math.random() * codes.length));
            }
            if (this.userRecord[code]) {
                return getCode(n);
            }
            return code;
        }
        const code = getCode(config.tokenSize);
        return code;
    }
    
    /** 定时检测过期的`token`并清理 */
    private checkRecord() {
        const check = () => {
            const now = Date.now();
            let isChange = false;
            for (const key in this.userRecord) {
                if (this.userRecord.hasOwnProperty(key)) {
                    const item = this.userRecord[key];
                    if (now - item.online > this.maxAge * 3600000) {
                        isChange = true;
                        delete this.userRecord[key];
                    }
                }
            }
            if (isChange) {
                this.write();
            }
        }
        // 定时检测
        setInterval(check, this.interval);
        check();
    }

    /**
     * 设置纪录并返回`token`
     * @param data 用户信息
     */
    setRecord(data: UserInfoType) {
        const token = this.getToken();
        data.online = Date.now();
        this.userRecord[token] = data;
        this.write();
        return token;
    }

    /**
     * 更新并检测`token`
     * @param token 
     * @description 这里可以做单点登录的处理,自行修改一下规则判断即可
     */
    updateRecord(token: string) {
        const result: JwtResultType = {
            message: "",
            success: false,
            info: null
        }

        if (!this.userRecord.hasOwnProperty(token)) {
            result.message = "token 已过期或不存在";
            return result;
        } 
        
        const userInfo = this.userRecord[token];

        const now = Date.now();

        if (now - userInfo.online > this.maxAge * 3600000) {
            result.message = "token 已过期";
            return result;
        }

        result.message = "token 通过验证";
        result.success = true;
        result.info = userInfo;

        // 更新在线时间并写入临时表
        // 这里优化一下,写入和更新的时间间隔为10分钟,避免频繁写入
        if (now - userInfo.online > this.interval) {
            this.userRecord[token].online = now;
            this.write();
        }
        
        return result;
    }

    /**
     * 从纪录中删除`token`纪录
     * @param token 
     * @description 主要是退出登录时用
     */
    removeRecord(token: string) {
        if (this.userRecord.hasOwnProperty(token)) {
            delete this.userRecord[token];
            this.write();
            return true;
        } else {
            return false;
        }
    }

    /**
     * 检测需要`token`的接口状态
     * @param context 
     */
    checkToken(context: TheContext) {
        const token: string = context.header.authorization;
        let fail = false;
        let info: ApiResult;

        if (!token) {
            fail = true;
            info = apiSuccess({}, "缺少token", 400);
        }

        if (token && token.length != config.tokenSize) {
            fail = true;
            info = apiSuccess({}, config.tokenTip, 400);
        }
        
        const state = this.updateRecord(token);

        if (!state.success) {
            fail = true;
            info = apiSuccess({}, state.message, 401);
        }

        // 设置 token 信息到上下文中给接口模块里面调用
        if (!fail) {
            context["theState"] = state;
        }

        return {
            fail,
            info
        }
    }

}

/** `jwt-token`模块 */
const jwt = new ModuleJWT();

export default jwt;

Подключение к базе данных и операциям интерфейса

Локальный сервис, который я использую здесь, построен с помощью (upupw), очень простой операции, инструмент таблицы базы данных navicat

адрес загрузки upupw

адрес загрузки navicatВ сети тоже много взломанного, можете скачать сами

  1. использовать здесьupupwПосле получения пароля учетной записи данных в командном окне,navicatСоздайте новое подключение к базе данных, заполните в соответствии с паролем учетной записи, будьте осторожны, чтобы не изменить порт по умолчанию, иначе подключение не будет выполнено.

  2. После подключения создайте новую базу данных в левой колонке с именемnode_ts, в кодеconfig.db.databaseВот и все, а таблица будет построена позже в этой колонке.

  1. начать строитьuser_formТаблица

Если вы не хотите создавать таблицу, вы можете напрямую добавить проектmysql/node_ts.sqlимпорт файловnode_tsбаза данных может

вернуться к проектуsrc/utils/mysql.tsПод файлом здесь инкапсулируется метод добавления, удаления, изменения и проверки базы данных, и все последующие операции с базой данных выполняются через этот метод.

import * as mysql from "mysql";         // learn: https://www.npmjs.com/package/mysql
import config from "../modules/Config";

interface queryResult {
    /** `state===1`时为成功 */
    state: number
    /** 结果数组 或 对象 */
    results: any
    /** 状态 */
    fields: Array<mysql.FieldInfo>
    /** 错误信息 */
    error: mysql.MysqlError
    /** 描述信息 */
    msg: string
}

/** 数据库 */
const pool = mysql.createPool({
    host: config.db.host,
    user: config.db.user,
    password: config.db.password,
    database: config.db.database
});

/**
 * 数据库增删改查
 * @param command 增删改查语句
 * @param value 对应的值
 */
export default function query(command: string, value?: Array<any>) {
    const result: queryResult = {
        state: 0,
        results: null,
        fields: null,
        error: null,
        msg: ""
    }
    return new Promise<queryResult>((resolve, reject) => {
        pool.getConnection((error: any, connection) => {
            if (error) {
                result.error = error;
                result.msg = "数据库连接出错";
                resolve(result);
            } else {
                const callback: mysql.queryCallback = (error: any, results, fields) => {
                    // pool.end(); 
                    connection.release();
                    if (error) {
                        result.error = error;
                        result.msg = "数据库增删改查出错";
                        resolve(result);
                    } else {
                        result.state = 1;
                        result.msg = "ok";
                        result.results = results;
                        result.fields = fields;
                        resolve(result);
                    }
                }

                if (value) {
                    pool.query(command, value, callback);
                } else {
                    pool.query(command, callback);
                }
            }
        });
    });
}

// learn: https://blog.csdn.net/gymaisyl/article/details/84777139	

Войти Зарегистрироваться Пользовательский модуль

mysqlоператор запросаuser_formТабличные поля

src/api/apiUser.tsпод файлом

import router from "./main";
import query from "../utils/mysql";
import jwt from "../modules/Jwt";
import config from "../modules/Config";
import {
    UserInfoType,
    TheContext,
    ApiResult
} from "../utils/interfaces";
import { apiSuccess, apiFail } from "../utils/apiResult";

// 注册
router.post("/register", async (ctx) => {
    /** 接收参数 */
    const params: UserInfoType = ctx.request.body;
    /** 返回结果 */
    let bodyResult: ApiResult;
    /** 账号是否可用 */
    let validAccount = false;
    // console.log("注册传参", params);

    if (!/^[A-Za-z0-9]+$/.test(params.account)) {
        return ctx.body = apiSuccess({}, "注册失败!账号必须由英文或数字组成", 400);
    }

    if (!/^[A-Za-z0-9]+$/.test(params.password)) {
        return ctx.body = apiSuccess({}, "注册失败!密码必须由英文或数字组成", 400);
    }

    if (!params.name.trim()) {
        params.name = "用户未设置昵称";
    }

    // 先查询是否有重复账号
    const res = await query(`select account from user_form where account='${ params.account }'`)

    // console.log("注册查询", res);

    if (res.state === 1) {
        if (res.results.length > 0) {
            bodyResult = apiSuccess({}, "该账号已被注册", 400);
        } else {
            validAccount = true;
        }
    } else {
        ctx.response.status = 500;
        bodyResult = apiFail(res.msg, 500, res.error);
    }

    // 再写入表格
    if (validAccount) {
        const res = await query("insert into user_form(account, password, username) values(?,?,?)", [params.account, params.password, params.name])
        if (res.state === 1) {
            bodyResult = apiSuccess(params, "注册成功");
        } else {
            ctx.response.status = 500;
            bodyResult = apiFail(res.msg, 500, res.error);
        }
    }
    
    ctx.body = bodyResult;
})

// 登录
router.post("/login", async (ctx) => {
    /** 接收参数 */
    const params: UserInfoType = ctx.request.body;
    /** 返回结果 */
    let bodyResult: ApiResult;
    // console.log("登录", params);
    if (!params.account || params.account.trim() === "") {
        return ctx.body = apiSuccess({}, "登录失败!账号不能为空", 400);
    }

    if (!params.password || params.password.trim() === "") {
        return ctx.body = apiSuccess({}, "登录失败!密码不能为空", 400);
    }

    // 先查询是否有当前账号
    const res = await query(`select * from user_form where account = "${ params.account }"`)
    
    console.log("登录查询", res);

    if (res.state === 1) {
        // 再判断账号是否可用
        if (res.results.length > 0) {
            const data: UserInfoType = res.results[0];
            // 最后判断密码是否正确
            if (data.password == params.password) {
                data.token = jwt.setRecord({
                    id: data.id,
                    account: data.account,
                    password: data.password
                });
                bodyResult = apiSuccess(data ,"登录成功");
            } else {
                bodyResult = apiSuccess({}, "密码不正确", 400);
            }
        } else {
            bodyResult = apiSuccess({}, "该账号不存在,请先注册", 400);
        }
    } else {
        ctx.response.status = 500;
        bodyResult = apiFail(res.msg, 500, res.error);
    }

    ctx.body = bodyResult;
})

// 获取用户信息
router.get("/getUserInfo", async (ctx: TheContext) => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const state = ctx["theState"];
    // /** 接收参数 */
    // const params = ctx.request.body;
    /** 返回结果 */
    let bodyResult: ApiResult;

    // console.log("getUserInfo", params, state);

    const res = await query(`select * from user_form where account = "${ state.info.account }"`)
    
    if (res.state === 1) {
        // 判断账号是否可用
        if (res.results.length > 0) {
            const data: UserInfoType = res.results[0];
            bodyResult = apiSuccess(data);
        } else {
            bodyResult = apiSuccess({}, "该账号不存在,可能已经从数据库中删除", 400);
        }
    } else {
        ctx.response.status = 500;
        bodyResult = apiFail(res.msg, 500, res.error);
    }

    ctx.body = bodyResult;
})

// 退出登录
router.get("/logout", ctx => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const token: string = ctx.header.authorization;
    /** 接收参数 */
    const params = ctx.request.body;

    console.log("logout", params, token);

    if (token.length != config.tokenSize) {
        return ctx.body = apiSuccess({}, config.tokenTip);
    }

    const state = jwt.removeRecord(token);

    if (state) {
        return ctx.body = apiSuccess({}, "退出登录成功");
    } else {
        return ctx.body = apiSuccess({}, "token 不存在", 400);
    }
})

Добавить, удалить, изменить и проверить функцию

Затем создайте еще один с именемtodo_formТаблица

Наконец создайте новыйapiTodo.tsИнтерфейсный модуль для добавления, удаления, изменения и проверки списка пользователей

import router from './main';
import query from '../modules/mysql';
import stateInfo from '../modules/state';
import { mysqlQueryType, mysqlErrorType, JwtResultType } from '../modules/interfaces';

// 获取所有列表
router.get('/getList', async (ctx) => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const state: JwtResultType = ctx['theState'];
    /** 返回结果 */
    let bodyResult = null;
    
    // console.log('getList');

    // 这里要开始连表查询
    await query(`select * from todo_form where user_id = '${ state.info.id }'`).then((res: mysqlQueryType) => {
        // console.log('/getList 查询', res.results);
        bodyResult = stateInfo.getSuccessData({
            list: res.results.length > 0 ? res.results : [] 
        });
    }).catch((err: mysqlErrorType) => {
        bodyResult = stateInfo.getFailData(err.message);
    })

    ctx.body = bodyResult;
})

// 添加列表
router.post('/addList', async (ctx) => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const state: JwtResultType = ctx['theState'];
    /** 接收参数 */
    const params = ctx.request.body;
    /** 返回结果 */
    let bodyResult = null;

    if (!params.content) {
        return ctx.body = stateInfo.getFailData('添加的列表内容不能为空!');
    }

    // 写入列表
    await query('insert into todo_form(content, time, user_id) values(?,?,?)', [params.content, new Date().toLocaleDateString(), state.info.id]).then((res: mysqlQueryType) => {
        // console.log('写入列表', res.results.insertId);
        bodyResult = stateInfo.getSuccessData({
            id: res.results.insertId
        }, '添加成功');
    }).catch((err: mysqlErrorType) => {
        // console.log('注册写入错误', err);
        bodyResult = stateInfo.getFailData(err.message);
    })
    
    ctx.body = bodyResult;
})

// 修改列表
router.post('/modifyList', async (ctx) => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const state: JwtResultType = ctx['theState'];
    /** 接收参数 */
    const params = ctx.request.body;
    /** 返回结果 */
    let bodyResult = null;

    if (!params.id) {
        return ctx.body = stateInfo.getFailData('列表id不能为空');
    }

    if (!params.content) {
        return ctx.body = stateInfo.getFailData('列表内容不能为空');
    }

    // 修改列表
    await query(`update todo_form set content='${params.content}', time='${new Date().toLocaleDateString()}' where list_id='${params.id}'`).then((res: mysqlQueryType) => {
        console.log('修改列表', res);
        if (res.results.affectedRows > 0) {
            bodyResult = stateInfo.getSuccessData({}, '修改成功');
        } else {
            bodyResult = stateInfo.getFailData('列表id不存在');
        }
    }).catch((err: mysqlErrorType) => {
        // console.log('注册写入错误', err);
        bodyResult = stateInfo.getFailData(err.message);
    })

    ctx.body = bodyResult;
})

// 删除列表
router.post('/deleteList', async (ctx) => {
    const checkInfo = jwt.checkToken(ctx);

    if (checkInfo.fail) {
        return ctx.body = checkInfo.info;
    }
    
    const state: JwtResultType = ctx['theState'];
    /** 接收参数 */
    const params = ctx.request.body;
    /** 返回结果 */
    let bodyResult = null;
    
    // 从数据库中删除
    await query(`delete from todo_form where list_id=${params.id} and user_id = ${state.info.id}`).then((res: mysqlQueryType) => {
        console.log('从数据库中删除', res);
        if (res.results.affectedRows > 0) {
            bodyResult = stateInfo.getSuccessData({}, '删除成功');
        } else {
            bodyResult = stateInfo.getFailData('当前列表id不存在或已删除');
        }
    }).catch((err: mysqlErrorType) => {
        console.log('从数据库中删除失败', err);
        bodyResult = stateInfo.getFailData(err.message);
    })

    ctx.body = bodyResult;
})

Сторонний интерфейс внутреннего запроса (расширенный)

  • этоnode.jsОдна из основных функций, в принципе иKoaЭто не имеет к этому никакого отношения, потому что актуальной информации, которую можно найти в Интернете, относительно немного, поэтому я кстати тоже привожу ее сюда.
  • Эта функция в основном используется, когда нам нужно получить некоторые сторонние данные, а затем вернуть их во внешний интерфейс, аналогично внутреннему интерфейсу апплета WeChat.

utilsСоздайте новый в каталогеrequest.ts

import * as http from "http";
import * as querystring from "querystring"
import * as zlib from "zlib"
import { ServeRequestResult } from "./interfaces";

/**
 * 服务端请求
 * - [基础请求参考](https://www.cnblogs.com/liAnran/p/9799296.html)
 * - [响应结果乱码参考](https://blog.csdn.net/fengxiaoxiao_1/article/details/72629577)
 * - [html乱码参考](https://www.microanswer.cn/blog/51)
 * - [node-http文档](http://nodejs.cn/api/http.html#http_class_http_incomingmessage)
 * @param options 请求配置
 * @param params 请求传参数据
 */
export default function request(options: http.RequestOptions, params: object = {}): Promise<ServeRequestResult> {
    /** 返回结果 */
    const info: ServeRequestResult = {
        msg: "",
        result: "",
        state: -1
    }

    /** 传参字段 */
    const data = querystring.stringify(params as any);

    if (data && options.method == "GET") {
        options.path += `?${data}`;
    }
    
    return new Promise((resolve, reject) => {
        const clientRequest = http.request(options, res => {
            // console.log("http.get >>", res);
            console.log(`http.request.statusCode: ${res.statusCode}`);
            console.log(`http.request.headers: ${JSON.stringify(res.headers)}`);

            // 因为现在自己解码,所以就不设置编码了。
            // res.setEncoding("utf-8");

            if (res.statusCode !== 200) {
                info.msg = "请求失败";
                info.result = {
                    statusCode: res.statusCode,
                    headers: res.headers
                }
                return resolve(info);
            }

            let output: http.IncomingMessage | zlib.Gunzip

            if (res.headers["content-encoding"] == "gzip") {
                const gzip = zlib.createGunzip();
                res.pipe(gzip);
                output = gzip;
            } else {
                output = res;
            }

            output.on("data", function(chunk) {
                console.log("----------> chunk >>");
                // info.result += chunk;
                // info.result = chunk;
                info.result += chunk.toString();
            });
            
            output.on("error", function(error) {
                console.log("----------> 服务端请求错误 >>", error);
                info.msg = error.message;
                info.result = error;
            })

            output.on("end", function() {
                console.log("---------- end ----------");
                if (res.complete) {
                    info.msg = "ok";
                    info.state = 1;
                    resolve(info);
                } else {
                    info.msg = "连接中断"
                    resolve(info);
                }
            });
            
        })
        
        if (data && options.method != "GET") {
            clientRequest.write(data)
        }

        clientRequest.end()
    })
}

Пример использованияИнтерфейс прогноза погоды здесь используется как демонстрация, если вы хотите запросить какие-то уникальные интерфейсы, вы можете настроить его самостоятельно, если хотите собирать данные.headersПросто смоделируйте

import { apiSuccess, apiFail } from "../utils/apiResult";
import request from "../utils/request";

// 请求第三方接口并把数据返回到前端
router.get("/getWeather", async (ctx, next) => {
    console.log("ctx.query >>", ctx.query);

    if (!ctx.query.city) {
        ctx.body = apiSuccess({}, "缺少传参字段 city", 400);
        return;
    }

    const res = await request({
        method: "GET",
        hostname: "wthrcdn.etouch.cn",
        path: "/weather_mini?city=" + encodeURIComponent(ctx.query.city),
        // headers: {
        //    "xxx": "asfdfewf"
        // }
    })

    // console.log("获取天气信息 >>", res);

    if (res.state === 1) {
        if (utils.checkType(res.result) === "string") {
            res.result = JSON.parse(res.result);
        }
        ctx.body = apiSuccess(res.result)
    } else {
        ctx.body = apiFail(res.msg, 500, res.result)
    }

})

Сборки проекта передаются в онлайн

  1. Купитьоблачный сервер ECSTencent Ali бесплатен;
  2. установить соответствующийnode.js,myql,gitи другие инструменты, которые по сути такие же, как те, которые устанавливаются на новый компьютер, за исключением того, что серверLinuxсистема, которая требует помощиMobaXtermЭтот инструмент используется для удаленного подключения, а затем установки и выполнения других операций, я не буду его здесь раскрывать, да и писать код не нужно...
  3. Создайте каталог, а затем используйтеgitПотяните код вниз,npm installЗатем запуститеnpm run startВсе так, но обнаруживается, что служба закрывается после выхода, поэтому надо установить другое управление процессами.pm2заменить наше руководствоnpm run start;
  4. установленpm2Затем вам нужно написать файл в текущем каталоге проектаpm2.jsonфайл запуска конфигурации, что-то вродеpackage.jsonОпять же, фрагмент кода выглядит следующим образом:
{
    "name": "node-serve", // 进程的名字
    "script": "npm run start" // 运行代码的命令
}

окончательное исполнениеpm2 start pm2.jsonКоманда запускает проект. Чтобы просмотреть и другие операции, обратитесь к следующим командам

# 启动任务
pm2 start pm2.json

# 强制停止所有的服务
pm2 kill

# 查看服务
pm2 list

# 查看指定进程日志,0是任务列表索引值
pm2 log 0

# 重启指定进程,0是任务列表索引值
pm2 restart 0

# 停止指定进程,0是任务列表索引值
pm2 stop 0

Последнее примечание

Вся фронтенд-отладка проекта пишется, а код фронтенд-интерфейса хранится вpublic/templateДалее, поскольку это совместное использование внутреннего кода, здесь нет демонстрации внешнего кода.