基建 —— 从0开发一个dns服务(一)

285 阅读6分钟

背景

搭建这个服务主要是为了手机端在局域网内通过PC端配置的hosts映射的域名访问本地启动的服务。

举个例子吧:
比如我们在开发一个h5项目,在PC端hosts配置域名映射(192.xxx.x.x www.bbb.com), 本地项目通过 192.xxx.x.x 启动,然后使用 www.bbb.com 就可以正常访问了,但是如果在局网内手机通过 www.bbb.com 访问,其实是无法访问的,因为 手机端是无法解析自定义的 www.bbb.com 域名的。因此就有一个大胆的项目,手机端不能配置 hosts,我们可以在PC端搭建一个域名解析服务,手机端可以设置DNS解析服务指向此服务就能顺利访问PC端的hosts映射的IP。

有了上面的想法,接下去我们就来尝试从0开发一个dns服务

此文章较长,分为两篇来写。本篇主要是为了搭建环境,以及开发dns服务前做准备。

如有兴趣可以耐性读完,这可是一个完整的 dns 服务哟

涉及技术点

项目目录

├── CHANGELOG.md
├── README.md
├── bin
|  └── dns-server.js
├── commitlint.config.js
├── package.json
├── rollup.config.js
├── src
|  ├── config
|  |  └── index.ts
|  ├── core
|  |  ├── parsing.ts
|  |  └── server.ts
|  ├── index.ts
|  ├── ioc
|  |  ├── interface.config.ts
|  |  └── types.ts
|  ├── types
|  |  ├── global.d.ts
|  |  └── interface.ts
|  └── utils
|     ├── consloe.ts
|     └── index.ts
├── tsconfig.json
└── yarn.lock

搭建项目

构建项目结构

# cd your project decatory
# 创建项目文件夹
mkdir dns-server && cd dns-server
# 创建源代码文件夹和入口文件
mkdir src && touch src/index.ts
# 初始化 package.json
yarn init -y

初始化 Typescript 环境

# 安装 typescript 并 创建 typescript配置文件 tsconfig.json
yarn add typescript -D && touch tsconfig.json

修改 tsconfig.json 配置

{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "downlevelIteration": true,
    "noEmit": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "types": ["node"],
    "sourceMap": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["src/**/*.test.ts"]
}

Typescript 在编译过程中会向 ./dist 目录输出 index.d.ts 的类型声明文件 修改 package.json

{
  
  "main": "dist/index.cjs.js",
  "types": "dist/index.d.ts",
}

初始化 Rollup 打包环境

# 安装 相关的包 并创建 rollup配置文件
yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs  rollup-plugin-json -D && touch rollup.config.js

修改 rollup.config.js 配置

import resolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import commonjs from '@rollup/plugin-commonjs';
import json from 'rollup-plugin-json';
import pkg from './package.json';

export default [
  {
    input: 'src/index.ts',
    output: [
      {
        file: pkg.main,
        format: 'cjs',
        sourcemap: true,
      },
    ],
    plugins: [resolve(), commonjs(), json(), typescript()],
  },
];

修改 package.json 配置

{
  "scripts": {
    "dev": "rollup -w -c",
    "build": "rm -rf dist && rollup -c",
  },
}

初始化 ESlint 环境

yarn add eslint -D && touch .eslintrc.json

修改 .eslintrc.json 配置

{
  "env": {
    "browser": true,
    "commonjs": true,
    "es2021": true
  },
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "rules": {
    "@typescript-eslint/no-explicit-any": ["off"]
  }
}

Prettier 代码自动格式化

yarn add prettier -D && touch .prettierrc.json

修改 .prettierrc.json 配置

{
  "printWidth": 150,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "bracketSpacing": true,
  "arrowParens": "avoid"
}

Git 初始化

git init && touch .gitignore

修改 .gitignore 配置

node_modules/
dist/
.DS_Store
.yarn-error.log

Husky Git 提交约束

yarn add husky@3.1.0 lint-staged  @commitlint/cli  @commitlint/config-conventional @commitlint/parse -D && touch commitlint.config.js

修改 package.json

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{ts,js}": [
      "node --max_old_space_size=8192 ./node_modules/.bin/prettier -w",
      "node --max_old_space_size=8192 ./node_modules/.bin/eslint --fix --color",
      "git add"
    ]
  },
}

修改 commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional'],
  parserPreset: {
    parserOpts: {
      // issue 前缀,自动识别 #1234 为 issue,可在 commit message 中写入关闭的问题 id
      issuePrefixes: ['#'],
    },
  },
  rules: {
    'header-max-length': [0, 'always', 100],
    'type-enum': [
      2,
      'always',
      [
        'feat', // feature 新功能,新需求
        'fix', // 修复 bug
        'docs', // 仅仅修改了文档,比如README, CHANGELOG, CONTRIBUTE等等
        'style', // 仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑
        'refactor', // 代码重构,没有加新功能或者修复bug
        'test', // 测试用例,包括单元测试、集成测试等
        'revert', // 回滚到上一个版本
        'perf', // 性能优化
        'chore', // 改变构建流程、或者增加依赖库、工具等,包括打包和发布版本
        'conflict', // 解决合并过程中的冲突
      ],
    ],
  },
};

根据 git commit 自动生成 CHANGELOG.md

yarn add standard-version -D

修改 package.json 添加脚本

{
  "scripts": {
    "standard": "standard-version"
  },
  "standard-version": {
    "types": [
      {
        "type": "feat",
        "section": "需求"
      },
      {
        "type": "fix",
        "section": "Bug 修复"
      },
      {
        "type": "perf",
        "section": "优化"
      },
      {
        "type": "chore",
        "hidden": true
      },
      {
        "type": "docs",
        "hidden": true
      },
      {
        "type": "style",
        "hidden": true
      },
      {
        "type": "refactor",
        "hidden": true
      },
      {
        "type": "test",
        "hidden": true
      },
      {
        "type": "conflict",
        "hidden": true
      },
      {
        "type": "revert",
        "hidden": true
      }
    ]
  },
  # commit url 默认取 package.json 中 repository.url
  "repository": {
    "type": "git",
    "url": "https://xxxxx.git" # 这里需要填写你的项目地址
  },
}

完整 package.json

{
  "name": "dns-server",
  "version": "0.0.1",
  "description": "在本地搭起简单的 DNS 解析服务",
  "author": "xxx",
  "license": "MIT",
  "main": "dist/index.cjs.js",
  "types": "dist/index.d.ts",
  "bin": {
    "dns-server": "bin/dns-server.js"
  },
  "scripts": {
    "dev": "rollup -w -c",
    "build": "rm -rf dist && rollup -c",
    "standard": "standard-version"
  },
  "standard-version": {
    "types": [
      {
        "type": "feat",
        "section": "需求"
      },
      {
        "type": "fix",
        "section": "Bug 修复"
      },
      {
        "type": "perf",
        "section": "优化"
      },
      {
        "type": "chore",
        "hidden": true
      },
      {
        "type": "docs",
        "hidden": true
      },
      {
        "type": "style",
        "hidden": true
      },
      {
        "type": "refactor",
        "hidden": true
      },
      {
        "type": "test",
        "hidden": true
      },
      {
        "type": "conflict",
        "hidden": true
      },
      {
        "type": "revert",
        "hidden": true
      }
    ]
  },
  "devDependencies": {
    "@commitlint/cli": "^17.0.3",
    "@commitlint/config-conventional": "^17.0.3",
    "@commitlint/parse": "^17.0.0",
    "@rollup/plugin-commonjs": "^22.0.2",
    "@rollup/plugin-node-resolve": "^13.3.0",
    "@types/yargs": "^17.0.11",
    "@typescript-eslint/eslint-plugin": "^5.33.0",
    "@typescript-eslint/parser": "^5.33.0",
    "eslint": "^8.21.0",
    "husky": "3.1.0",
    "lint-staged": "^13.0.3",
    "prettier": "^2.7.1",
    "rollup": "^2.77.3",
    "rollup-plugin-json": "^4.0.0",
    "rollup-plugin-typescript2": "^0.32.1",
    "standard-version": "^9.5.0",
    "typescript": "^4.7.4"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{ts,js}": [
      "node --max_old_space_size=8192 ./node_modules/.bin/prettier -w",
      "node --max_old_space_size=8192 ./node_modules/.bin/eslint --fix --color",
      "git add"
    ]
  },
  "repository": {
    "type": "git",
    "url": "https://xxx.git"
  },
  "dependencies": {
    "cacheable-lookup": "^6.1.0",
    "inversify": "^6.0.1",
    "reflect-metadata": "^0.1.13",
    "yargs": "^17.5.1"
  }
}

自定义Console

主要是为了更改console输出的颜色(只为好看)

安装 chalk

yarn add chalk

文件路径:src/utils/consloe.ts

抽离 重写 console 方法

const overloadConsole = (methods: TMethod[], output: TOutput) => {
  const rawMethods: TRewMethods = {};
  methods.forEach(method => {
    if (typeof console[method] !== 'function') return;
    // 复制原生方法 
    rawMethods[method] = console[method];
    // 重写
    console[method] = (...args: any[]) => {
      output({ method, args, rawMethods });
    };
  });
};

给 console 一点颜色

// 重写 'log', 'warn', 'info' 这放个方法
const logMethods: TMethod[] = ['log', 'warn', 'info'];
export const makeConsoleColored = () => {
  overloadConsole(logMethods, ({ method, args, rawMethods }) => {
    const fns: Partial<Record<TMethod, Chalk>> = {
      warn: chalk.yellowBright,
      info: chalk.blueBright,
      error: chalk.redBright,
    };

    const fn = fns[method] || ((arg: any) => arg);

    rawMethods[method](...args.map(arg => (typeof arg === 'string' ? fn(arg) : arg)));
  });
};

关闭console 日志输出

export const disabledConsoleOutput = () => {
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  overloadConsole(logMethods, () => {});
};

拓展 console方法

// 主要是为了避开 关闭 console ,无法使用 console 的方法
export const expandConsoleOutput = () => {
  console.tip = console.log.bind(console.log);
};

完整 src/utils/consloe.ts

import chalk, { Chalk } from 'chalk';

declare const console: any;
type TRewMethods = { [M: string]: (...args: any) => void };
type TMethod = keyof Console;
type TOutput = (p: { method: TMethod; args: any[]; rawMethods: TRewMethods }) => void;

const logMethods: TMethod[] = ['log', 'warn', 'info'];

const overloadConsole = (methods: TMethod[], output: TOutput) => {
  const rawMethods: TRewMethods = {};

  methods.forEach(method => {
    if (typeof console[method] !== 'function') return;
    rawMethods[method] = console[method];
    console[method] = (...args: any[]) => {
      output({ method, args, rawMethods });
    };
  });
};

export const makeConsoleColored = () => {
  overloadConsole(logMethods, ({ method, args, rawMethods }) => {
    const fns: Partial<Record<TMethod, Chalk>> = {
      warn: chalk.yellowBright,
      info: chalk.blueBright,
      error: chalk.redBright,
    };

    const fn = fns[method] || ((arg: any) => arg);

    rawMethods[method](...args.map(arg => (typeof arg === 'string' ? fn(arg) : arg)));
  });
};

export const disabledConsoleOutput = () => {
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  overloadConsole(logMethods, () => {});
};

export const expandConsoleOutput = () => {
  console.tip = console.log.bind(console.log);
};

创建容器和类型

class标识,用于指定DI寻找对应的class

src/ioc/types.ts

export const TYPES = {
  Parsing: Symbol('PARSING'),
  Server: Symbol('Server'),
};

DNS 解析服务 -- src/core/parsing.ts

import { inject, injectable } from "inversify";
import { TYPES } from "../ioc/types";
import { IServer } from "./server";
declare const console: any;

export interface IParsing {
  createServer(address: string | undefined, port: number): void;
}

// 告知依赖注入inversify这个类需要被注册到容器中
@injectable()
class Parsing implements IParsing {
  // 注入 Server 实例
  @inject(TYPES.Server) Server: IServer;
}

export default Parsing;

DNS request & response -- src/core/server.ts

import { injectable } from "inversify";

export interface IServer {}

// 告知依赖注入inversify这个类需要被注册到容器中
@injectable()
class Server implements IServer {}

export default Server;

容器注册

import 'reflect-metadata';
import { Container } from 'inversify';
import Parsing, { IParsing } from '../core/parsing';
import { TYPES } from './types';
import Server, { IServer } from '../core/server';

// 创建容器
const container = new Container();

// 绑定 Parsing 到 container;TYPES.Parsing 是 Parsing的标识,便于注入
container.bind<IParsing>(TYPES.Parsing).to(Parsing);
container.bind<IServer>(TYPES.Server).to(Server);

export { container };

参考