Node入门(一):Koa 搭建 web 服务器

2,808 阅读5分钟

其实从我自学转前端之后,大概2019年,就有听说 Node.js 了。期间,也尝试过跟着网上的教程,写个 web 服务器之类,写完没多久又忘了~

后来一直在一家公司待着,写业务、写业务、写业务,就逐渐麻痹了自己。

业务是做得还不错了,但是知识面真的太窄了~

都 2022 年了,我居然还不会 Node。深刻反省,决定好好学习实践一下 Node.js 了~

只要开始了,就不算晚吧?!(安慰自己)

一、起个 Vite + Vue3 项目

参考 Vite 官网:vitejs.cn/guide/#scaf…

二、安装依赖

(一)安装 koa

基于 Node.js 的 web 开发框架,支持 async 语法。

文档: wohugb.gitbooks.io/koajs/conte… koa.bootcss.com/#applicatio…

安装:

npm i koa --save

(二)安装koa-router

用来处理 http 请求的 url。

文档: wohugb.gitbooks.io/koajs/conte…

安装:

npm i koa-router --save

(三)安装 koa-body-parser

用来将 post 请求的请求体解析为 json 格式。

文档: www.npmjs.com/package/koa…

安装:

npm install koa-bodyparser --save

三、搭 koa 服务器

先在项目的 src 目录下新建一个文件夹 server,用来放我们的 web 服务器相关的代码。

// src/server/index.js

const Koa = require('koa'); // 引入 koa
const router = require('koa-router')(); // 引入 koa-router
const bodyParser = require('koa-body-parser'); // 引入 koa-body-parser
const app = new Koa(); // 起一个 koa 服务器

app.use(bodyParser()); // 还没派上用场,先放这儿

// 这两个请求处理,是用来测试的
router.get('/', async (ctx, next) => {
    // ctx 是对请求的 request 和 response 对象的封装
    ctx.response.body = '<h1>Home</h1>';
})
router.get('/api/login', async (ctx, next) => {
    ctx.response.body = '<h1>Login</h1>';
})

app.use(router.routes());

app.listen(1129); // 设置服务器的端口
console.log('App started at port 1129...');

接下来,切换(不切换也行)到 server 目录下,运行服务器~

cd src/server
node index.js

可以在终端看到服务器运行了

image.png

接下来,测试一下,我们上面写的两个 get 请求有没有成功。

在浏览器打开 http://localhost:1129http://localhost:1129/api/login

image.png image.png

很好,没问题~

四、控制器及接口处理函数封装

一般,项目中,接口可以分为好多不同的模块的,比如:处理用户登录/信息、订单、商品、购物车等等。如果把所有的接口请求处理函数,都放在上面的 index.js,文件就会显得非常庞大且杂乱。所以:

  1. 把每个模块单独拎出来

    例如,我的 demo 项目中,在 server/api 目录下,新建了 user.js 和 invoice.js

  2. 封装一个控制器(controller),将所有的模块里的接口处理函数统一添加到 web 服务器中

// src/server/spi/user.js 处理用户相关请求

// 用户列表(因为没有连数据库,先用假数据)
let userList = [
    { username: 'Aubrey', password: '123456' },
    { username: 'Gabriel', password: '654321' },
];

const login = async (ctx, next) => {
    let { username, password } = ctx.request.body;
    let user = userList.find(item => item.username === username && item.password === password )
    
    if (user) {
        ctx.response.status = 200
        ctx.cookies.set(SESSION_ID, cardId)
        ctx.response.body = { code: 0, message: '登录成功' }
    } else {
        ctx.response.body = { code: -1, error: `用户名 ${username} 不存在,或者密码错误` }
    }
}

/**
 * 处理用户登录成功/失败
 */
const loginSuccess = async (ctx, next) => {
    // ctx.body === ctx.response.body 
    ctx.body = `类型:${ctx.query.type)}`
}
const loginFail = async (ctx, next) => {
    ctx.body = `类型:${ctx.query.type}`
}

module.exports = {
    'POST /api/login': login,
    'GET /api/login/success': loginSuccess,
    'GET /api/login/fail': loginFail
}
// src/server/api/invoice.js 处理用户发票信息模块

// 发票列表
const getInvoiceList = [
    { id: '2022031801', name: '淘宝购物', amount: '172.50', time: '2022-03-18 13:53' },
    { id: '2022031802', name: '美团买菜', amount: '112.25', time: '2022-03-17 13:53' },
    { id: '2022031803', name: '考拉海购', amount: '612.67', time: '2022-03-16 13:53' },
    { id: '2022031804', name: '京东吉他', amount: '812.19', time: '2022-03-15 13:53' },
]

const invoiceList = async (ctx, next) => {
    ctx.response.status = 200
    ctx.response.body = { code: 0, invoiceList }
}

module.exports = {
    'POST /invoice/list': getInvoiceList
}
// src/server/controller.js 控制器

const fs = require('fs'); // 引入 node 的 文件操作模块 fs
const router = require('koa-router')(); // 引入 koa-router

/**
 * 添加 GET/POST 请求
 * @param urlMapping 请求
 */
function handleUrl(urlMapping) {
    Object.entries(urlMapping).forEach(([url, handler]) => {
        if (url.startsWith('GET')) {
            let path = url.substring(4);
            router.get(path, handler);
            return;
        }
        if (url.startsWith('POST')) {
            let path = url.substring(5);
            router.post(path, handler);
            return;
        }
        console.log(`invalid URL: ${url}`);
    })
}

/**
 * 读取所有 API 处理方法
 */
function addController (dir) {
    const files = fs.readdirSync(__dirname + dir);
    const jsFiles = files.filter(fileName => {
        return fileName.endsWith('.js');
    })

    for (let file of jsFiles) {
        console.log(`Process controller: ${file}...`);
        let mapping = require(__dirname + dir + file);
        handleUrl(mapping);
    }
}

module.exports = function (dir) {
    const controllerDir = dir || '/api/'; // 这里可以配置默认文件夹
    addController(controllerDir);
    return router.routes();
}

同时,需要把 index.js 代码简化了

// src/server/index.js

const Koa = require('koa');
const bodyParser = require('koa-body-parser');
const app = new Koa();
const controller = require('../server/controller');

app.use(bodyParser());

app.use(controller());

app.listen(1129);
console.log('App started at port 1129...')

然后,重启服务器

node index.js

可以看到终端的输出如下:

image.png

五、前端发送请求

上面,我们虽然把服务器代码写完了,但是那几个 POST 请求,还没验证过。

接下来,我们就在项目里的 login.vue 文件里发送请求到服务器测试啦~

注意:先 npm i axios --save 安装一下 axios

  1. 首先,随便创建个 login.vue 页面
<template>
  <div class="login">
    <h1>Login</h1>
    <div class="form">
      <label for="username">Username</label>
      <input v-model="formData.username" type="text" name="username" class="input" id="username" />
      <label for="password">Password</label>
      <input v-model="formData.password" type="password" class="input" id="password" />
      <button class="btn" @click="login">Login</button>
    </div>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "Login",
  data () {
    return {
      formData: {
        username: null,
        password: null,
      }
    }
  },
  methods: {
    login () {
      axios.post('/api/login', this.formData).then(res => {
        // 省略校验用户名、密码等代码
        if (res.data.code === 0) {
            alert(res.data.message);
        } else {
          alert(res.data.message);
        }
      }).catch(error => {
        console.error(error);
      })
    }
  }
}
</script>

<style scoped>
.form {
  display: flex;
  flex-flow: column;
  max-width: 400px;
  margin: 0 auto;
}
</style>
  1. 配置代理 因为浏览器的同源策略,我们在 localhost:3001 下 请求 localhost:1129,是会被拦截的。 所以我们需要在 vite.config.js 里配置下代理。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const path = require('path')

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
  return {
    plugins: [vue()],
    resolve: {
      // 别名
      alias: {
        '@': path.resolve(__dirname, '/src'),
      },
      // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会影响 IDE 和类型支持。
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    server: {
      port: '3001', // 默认端口是 3000,这里改成了 3001
      // 配置代理
      proxy: {
        '/api': {
          target: 'http://localhost:1129',
        }
      },
    }
  }
})
  1. 用正确的用户名、密码发送请求 可以看到,请求成功了,也返回了我们在 user.js 里设置的数据 image.png image.png image.png image.png image.png

  2. 用错误的用户名、密码发送请求

image.png

image.png

  1. 测试一下我们写的 GET 请求 将上面 login.vue 中 login 方法 改为如下:
login () {
      axios.post('/api/login', this.formData).then(res => {
        // 省略校验用户名、密码等代码
        if (res.data.code === 0) {
          // 一般,这里会用 vue-router 来控制页面的跳转行为
          // 但是这里为了触发 web 服务器的 GET 请求做演示
          // 因为我们没有登录成功页面,所以会跳转至首页
          location.href = `/api/login/success?type=登录成功`;
        } else {
          // 登录失败也跳转至首页
          location.href = `/api/login/fail?type=登录失败`;
        }
      }).catch(error => {
        console.error(error);
      })
    }
  }

我们再次分别用正确/错误的用户名、密码去登录,会分别跳转至下面两个页面。

说明,成功了~

image.png

image.png

六、总结

学到这里,以后实际开发中,后端接口没通前,我们都可以自己起个服务器,然后 mock 一些数据,实现各种请求,尤其是分页加载数据功能~

七、参考文章

www.liaoxuefeng.com/wiki/102291…