TTask - Part 3 创建 server 项目

Published on
7 min read

在之前的两篇文章里,我们从零搭建了 React Web 项目,Part 3 的任务是创建服务端项目。

项目初始化与 Webpack 配置

apps 目录下新建 server 文件夹,在 server 目录下执行 pnpm init 初始化。

安装依赖

# @babel 依赖
pnpm add -D @babel/cli @babel/core @babel/preset-env @babel/preset-typescript --filter server
# typescript 以及 Webpack 插件
pnpm add -D fork-ts-checker-webpack-plugin tsconfig-paths-webpack-plugin typescript --filter server
# Webpack
pnpm add -D webpack webpack-bundle-analyzer webpack-cli webpack-dev-server webpack-merge --filter server

注意区分 --filter server 依赖需要安装到 server 子项目。

安装 koa

pnpm add koa --filter server

pnpm add @types/express -D --filter server

接下来创建一个简单的服务,src/app.ts

import Koa, { Context, Next } from 'koa';

const app = new Koa();

const { PORT = 3000 } = process.env;

app.use(async (ctx: Context, _next: Next) => {
  ctx.body = 'Hello world';
});

app.listen(PORT, () => {
  console.log('Server running at http://localhost:', PORT);
});

添加 Webpack 配置

实际代码库中没有添加 Webpack 配置,由于 server 代码,只有 Typescript,还是决定开发环境下使用 ts-node 直接运行,实际发布使用 tsc 编译

server 目录下新建 webpack 文件夹,存放 server 项目的 Webpack 配置。公用的 Webpack 之前是为 web 项目配置,其中包含例如 html-webpack-pluginmini-css-extract-pluginserver 项目不需要的配置,需要精简 Webpack 并能够同时支持 webserver 项目。

之前有过度设计的嫌疑了,Webpack 在 server 项目与 web 项目上的配置有较大差异,目前整体项目还没必要抽离公共的 Webpack 配置,直接回归到为子项目单独配置 Webpack

server/webpack 目录下添加 webpack.base.js 配置文件,并添加如下配置。

const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = {
  name: 'server',
  entry: {
    server: path.join(process.cwd(), './src/app.ts'),
  },
  output: {
    path: path.join(process.cwd(), './dist'),
    filename: '[name].js',
    clean: true,
  },
  plugins: [new ForkTsCheckerWebpackPlugin()],
  resolve: {
    extensions: ['.ts', '.js'],
    plugins: [new TsconfigPathsPlugin()],
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: require.resolve('babel-loader'),
        options: {
          cacheDirectory: true,
          rootMode: 'upward',
        },
      },
    ],
  },
};

添加 webpack.config.dev.js 文件,使用 webpack-merge 工具,整合 webpack.base.js 配置

// webpack.config.dev.js
const { merge } = require('webpack-merge');
const base = require('./webpack.base');

module.exports = merge(base, {
  mode: 'development',
  watch: true,
  resolve: {
    alias: {},
  },
});

server/package.json 中添加如下的脚本

{
  "scripts": {
    "start": "NODE_ENV=development webpack --config webpack/webpack.config.dev.js"
  }
}

web 项目一样,在根目录下的 package.json 中新增启动服务端项目的脚本

{
  "scripts": {
    "start:server": "pnpm --filter server start"
  }
}

执行 pnpm start:server 打印的日志报错,这是因为 Webpack 的 target 配置的默认值,当项目中存在 browserslist 配置时值为 browserslist,否则为 web。针对服务端项目,我们需要设置 targetnode,重新执行命令,错误消失了。不过仍然还有一条如下的警告

Critical dependency: the request of a dependency is an expression

查阅资料之后发现,设置了 target:node,意味着 Webpack 需要打包核心模块,但不打包 node_modules 中的模块。实际上,光靠这一个配置项是不够的,需要借助 webpack-node-externals 帮助我们在打包时忽略 node_modules

Webpack 打包结束之后,我们可以通过在中断执行 node dist/server.js 来启动项目,使用 nodemon 能够实现监听打包后文件的变化,自动重启 API 服务。

pnpm add -D nodemon nodemon-webpack-plugin --filter server

执行 nodemon ./dist/server.js 前必须确保 Webpack 编译结束,通过 nodemon-webpack-plugin 插件可以帮助我们监听 Webpack 编译完成之后,自动启动 nodemon 服务。执行 pnpm start:server,之后访问 http://localhost:3000 就能看到返回的 Hello world

简化! 使用 tsc

server 项目为纯 Typescript 代码,使用 Webpack 总觉得有点杀鸡用牛刀的感觉,上文提到的 Webpack 配置,暂且就当学习记录一下。这一部分简单记录一下开发环境下使用 ts-node 的配置。

安装 ts-node 等依赖

pnpm add -D ts-node @types/node --filter server

修改运行 npm scripts

{
  "scripts": {
    "start": "nodemon --ignore tests/ --watch src -e ts --exec \"node --inspect --require ts-node/register src/app.ts\""
  }
}

使用 --inspect 开启 nodejs 的调试模式

添加路由

相比于 Express 的大而全,Koa 更轻量,例如 Koa 并不提供开箱即用的路由配置选项。如果需要添加路由管理,需要借助 @koa/router

安装 @koa/router

pnpm add @koa/router --filter server

pnpm add @types/koa__router -D --filter server

规定服务端项目文件结构

在搜索了多个 nodejs 服务端示例项目后,总结常见的路由与业务逻辑分离的文件结构,大致如下

src/
  controllers/          # 业务逻辑
  routes/               # 定义路由
    index.ts
  app.ts                # 项目 entry

接下来在 routes/index.ts 下添加如下代码

import { Context } from 'koa';
import Router from '@koa/router';

// 统一添加路由 /api 前缀
const router = new Router({
  prefix: '/api',
});

router.get('/', (ctx: Context) => {
  ctx.body = 'Hello world';
});

export default router;

修改 src/app.ts 文件内容,导入 routes/index.ts 文件中定义的路由

import Koa from 'koa';
import router from './routes';

const app = new Koa();

const { PORT = 3000 } = process.env;

app
  .use(router.routes())
  .use(router.allowedMethods())
  .listen(PORT, () => {
    console.log('Server running at http://localhost:', PORT);
  });

重启服务,在浏览器中打开 http://localhost:3000/api 就可以看到打印的 Hello world 了。

未完成的工作

到这里创建 server 项目就告一段落了,当然一个完整的服务端项目还有许多工作许多配置需要完成。

  1. 使用 Decorators 装饰器 简化 route 的定义
  2. prisma 与数据库配置
  3. 编写测试 API 并在 web 项目中实际调用
  4. API 添加单元测试