背景
搭建这个服务主要是为了手机端在局域网内通过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 };