├── LICENSE ├── README.md ├── short-url-server ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── nest-cli.json ├── nodemon.json ├── ormconfig.json ├── package-lock.json ├── package.json ├── src │ ├── app.module.ts │ ├── common │ │ ├── __tests__ │ │ │ └── stringUtil.spec.ts │ │ ├── config.ts │ │ ├── errorCode.ts │ │ └── stringUtil.ts │ ├── controllers │ │ ├── __tests__ │ │ │ └── app.controller.spec.ts │ │ └── app.controller.ts │ ├── entities │ │ └── url.ts │ ├── main.ts │ └── services │ │ ├── __tests__ │ │ └── app.service.spec.ts │ │ └── app.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── tslintrc.json ├── short-url-web ├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── manifest.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ └── logo512.png ├── src │ ├── App.tsx │ ├── common │ │ └── config.ts │ ├── index.tsx │ ├── page │ │ ├── index.tsx │ │ └── request.ts │ ├── react-app-env.d.ts │ └── style │ │ └── css │ │ └── index.css ├── tsconfig.json └── webpack.config.js └── short-url.code-workspace /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eric 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 短连接生成系统 3 | 4 | Powered by NestJS + React 5 | 6 | # 后端 short-url-server 7 | 8 | ## 技术栈 9 | NestJs + TypeScript + TypeORM + Sqlite 10 | 11 | ## Run it 12 | ``` 13 | npm i 14 | npm start 15 | ``` 16 | will run server in open http://localhost:3000/ 17 | 18 | ## Test 19 | ``` 20 | npm run test 21 | npm run test:cov 22 | ``` 23 | 24 | # 测试覆盖率 25 | ``` 26 | --------------------|---------|----------|---------|---------|------------------- 27 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 28 | --------------------|---------|----------|---------|---------|------------------- 29 | All files | 96.25 | 78.57 | 100 | 95.52 | 30 | common | 100 | 100 | 100 | 100 | 31 | config.ts | 100 | 100 | 100 | 100 | 32 | errorCode.ts | 100 | 100 | 100 | 100 | 33 | stringUtil.ts | 100 | 100 | 100 | 100 | 34 | controllers | 96 | 83.33 | 100 | 94.74 | 35 | app.controller.ts | 96 | 83.33 | 100 | 94.74 | 16 36 | entities | 100 | 100 | 100 | 100 | 37 | url.ts | 100 | 100 | 100 | 100 | 38 | services | 93.1 | 66.67 | 100 | 91.67 | 39 | app.service.ts | 93.1 | 66.67 | 100 | 91.67 | 14,32 40 | --------------------|---------|----------|---------|---------|------------------- 41 | ``` 42 | 43 | # 短连接生成机制 44 | 采用数据库sum自增的方式, 将sum总数转变为62进制的字符串 45 | 46 | 对于不超过8位的短链接,最多可以存放 62^8 ≈ 218,340,105,584,896 个链接 47 | 48 | # 数据库 49 | ``` 50 | export class Url { 51 | @PrimaryColumn('varchar', { nullable: false }) 52 | public shortUrl: string; 53 | 54 | @Column("text", { nullable: false}) 55 | public longUrl: string; 56 | } 57 | ``` 58 | 59 | 60 | # 结构框架 61 | ``` 62 | ── src 63 | │ ├── app.module.ts 64 | │ ├── common 通用模块 65 | │ │ ├── __tests__ 66 | │ │ │ └── stringUtil.spec.ts 67 | │ │ ├── config.ts 68 | │ │ ├── errorCode.ts 69 | │ │ └── stringUtil.ts 70 | │ ├── controllers 路由 71 | │ │ ├── __tests__ 72 | │ │ │ └── app.controller.spec.ts 73 | │ │ └── app.controller.ts 74 | │ ├── entities ORM实例 75 | │ │ └── url.ts 76 | │ ├── index.ts 77 | │ ├── main.ts 78 | │ ├── middlewares 79 | │ ├── migration 80 | │ └── services 服务层,负责entity和controller的通信 81 | │ ├── __tests__ 82 | │ │ └── app.service.spec.ts 83 | │ └── app.service.ts 84 | ``` 85 | 86 | 87 | # TODO 88 | * 在10进制到62进制的转换中, 对于32位的系统, js会出现溢出,需用BigNumber来操作 89 | * 增加输入合法性的验证 90 | 91 | 92 | # 前端 short-url-web 93 | 94 | ## 技术栈 95 | React + TypeScript + Axios + AntD 96 | 97 | 98 | # TODO 99 | * 本来想配置less的,报错了, 先发一版再说,回头改 100 | 101 | ## Run it 102 | ``` 103 | npm i 104 | npm start 105 | ``` 106 | open http://localhost:8080/ 107 | -------------------------------------------------------------------------------- /short-url-server/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* -------------------------------------------------------------------------------- /short-url-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@react-native-community', 'plugin:jest/recommended'], 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | rules: { 7 | 'prettier/prettier': [ 8 | 'error', 9 | { 10 | endOfLine: 'auto', 11 | }, 12 | ], 13 | 'max-len': ['warn', { code: 160 }], 14 | 'no-shadow': 0, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /short-url-server/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ 7 | dist/ 8 | coverage/ 9 | *.sqlite -------------------------------------------------------------------------------- /short-url-server/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | endOfLine: "auto", 7 | printWidth: 120 8 | }; 9 | -------------------------------------------------------------------------------- /short-url-server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /short-url-server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts", 8 | "src/__tests__/*" 9 | ], 10 | "exec": "ts-node src/main.ts" 11 | } -------------------------------------------------------------------------------- /short-url-server/ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sqlite", 3 | "database": "url.sqlite", 4 | "synchronize": true, 5 | "logging": false, 6 | "entities": [ 7 | "src/entity/**/*.ts", 8 | "src/entities/**/*.ts" 9 | ], 10 | "migrations": [ 11 | "src/migration/**/*.ts" 12 | ], 13 | "subscribers": [ 14 | "src/subscriber/**/*.ts" 15 | ], 16 | "cli": { 17 | "entitiesDir": "src/entity", 18 | "migrationsDir": "src/migration", 19 | "subscribersDir": "src/subscriber" 20 | } 21 | } -------------------------------------------------------------------------------- /short-url-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "short-url", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nodemon", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", 17 | "eslint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^7.0.0", 26 | "@nestjs/core": "^7.0.0", 27 | "@nestjs/platform-express": "^7.0.0", 28 | "mysql": "^2.14.1", 29 | "nodemon": "^2.0.4", 30 | "reflect-metadata": "^0.1.10", 31 | "rimraf": "^3.0.2", 32 | "rxjs": "^6.5.4", 33 | "sqlite3": "^4.0.3", 34 | "tslint": "^6.1.2", 35 | "typeorm": "0.2.25" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^7.0.0", 39 | "@nestjs/schematics": "^7.0.0", 40 | "@nestjs/testing": "^7.0.0", 41 | "@types/express": "^4.17.3", 42 | "@types/jest": "25.2.3", 43 | "@types/node": "^8.0.29", 44 | "@types/supertest": "^2.0.8", 45 | "eslint-plugin-jest": "^24.1.3", 46 | "@react-native-community/eslint-config": "^1.1.0", 47 | "@typescript-eslint/eslint-plugin": "^2.27.0", 48 | "@typescript-eslint/parser": "^2.27.0", 49 | "eslint": "^7.1.0", 50 | "jest": "26.0.1", 51 | "prettier": "^1.19.1", 52 | "supertest": "^4.0.2", 53 | "ts-jest": "26.1.0", 54 | "ts-loader": "^6.2.1", 55 | "ts-node": "3.3.0", 56 | "tsconfig-paths": "^3.9.0", 57 | "tslint-config-prettier": "^1.18.0", 58 | "typescript": "3.3.3333" 59 | }, 60 | "jest": { 61 | "moduleFileExtensions": [ 62 | "js", 63 | "json", 64 | "ts" 65 | ], 66 | "rootDir": "src", 67 | "testRegex": ".spec.ts$", 68 | "transform": { 69 | "^.+\\.(t|j)s$": "ts-jest" 70 | }, 71 | "coverageDirectory": "../coverage", 72 | "testEnvironment": "node" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /short-url-server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './controllers/app.controller'; 3 | import { AppService } from './services/app.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [AppController], 8 | providers: [AppService], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /short-url-server/src/common/__tests__/stringUtil.spec.ts: -------------------------------------------------------------------------------- 1 | import { string10to62, validURL } from '../stringUtil'; 2 | 3 | describe('AppController', () => { 4 | describe('root', () => { 5 | it('should convert 10-nim to 62-num"', async () => { 6 | expect(string10to62(763876481314)).toBe('drNVRSi'); 7 | }); 8 | 9 | it('should valid url"', async () => { 10 | expect(validURL('ssss')).toBe(false); 11 | expect(validURL('baidu.com')).toBe(true); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /short-url-server/src/common/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | SERVER_URL: 'www.s.cn', 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /short-url-server/src/common/errorCode.ts: -------------------------------------------------------------------------------- 1 | const errorCodes = { 2 | SUCDESS: 0, 3 | 4 | URL_NOT_EXIST: 100, 5 | URL_NOT_VALID: 101, 6 | }; 7 | 8 | export default errorCodes; 9 | -------------------------------------------------------------------------------- /short-url-server/src/common/stringUtil.ts: -------------------------------------------------------------------------------- 1 | function string10to62(number) { 2 | const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split(''); 3 | const radix = chars.length; 4 | let qutient = +number; 5 | const arr = []; 6 | do { 7 | const mod = qutient % radix; 8 | qutient = (qutient - mod) / radix; 9 | arr.unshift(chars[mod]); 10 | } while (qutient); 11 | return arr.join(''); 12 | } 13 | 14 | function validURL(str) { 15 | const pattern = new RegExp( 16 | '^(https?:\\/\\/)?' + // protocol 17 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name 18 | '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address 19 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path 20 | '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string 21 | '(\\#[-a-z\\d_]*)?$', 22 | 'i', 23 | ); // fragment locator 24 | 25 | return str && !!pattern.test(str); 26 | } 27 | 28 | export { string10to62, validURL }; 29 | -------------------------------------------------------------------------------- /short-url-server/src/controllers/__tests__/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from '../app.controller'; 3 | import { AppService } from '../../services/app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | let appService: AppService; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | 15 | appController = app.get(AppController); 16 | appService = app.get(AppService); 17 | }); 18 | 19 | describe('root', () => { 20 | it('should insert url', async () => { 21 | const shortUrl = 'www.s.cn/a'; 22 | const longUrl = 'www.baidu.com/looooooooooooooogurl'; 23 | jest.spyOn(appService, 'insertUrl').mockImplementation(async (): Promise => Promise.resolve(shortUrl)); 24 | 25 | const response = { 26 | json: r => r, 27 | status: () => response, 28 | }; 29 | 30 | const resp = await appController.insertUrl(longUrl, response); 31 | expect(resp.data).toBe(shortUrl); 32 | expect(resp.code).toBe(0); 33 | }); 34 | 35 | it('should find url', async () => { 36 | const shortUrl = 'www.s.cn/a'; 37 | const longUrl = 'www.baidu.com/looooooooooooooogurl'; 38 | jest.spyOn(appService, 'findUrl').mockImplementation(async (): Promise => Promise.resolve(longUrl)); 39 | 40 | const response = { 41 | json: r => r, 42 | status: () => response, 43 | }; 44 | 45 | const resp = await appController.findUrl(shortUrl, response); 46 | expect(resp.data).toBe(longUrl); 47 | expect(resp.code).toBe(0); 48 | }); 49 | 50 | it('should find nothing', async () => { 51 | const shortUrl = 'www.s.cn/a'; 52 | jest.spyOn(appService, 'findUrl').mockImplementation(async (): Promise => Promise.resolve(null)); 53 | 54 | const response = { 55 | json: r => r, 56 | status: () => response, 57 | }; 58 | 59 | const resp = await appController.findUrl(shortUrl, response); 60 | expect(resp.code).toBe(100); 61 | }); 62 | 63 | it('should know invalid url', async () => { 64 | const shortUrl = 'ssss'; 65 | const response = { 66 | json: r => r, 67 | status: () => response, 68 | }; 69 | 70 | const resp = await appController.findUrl(shortUrl, response); 71 | expect(resp.code).toBe(101); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /short-url-server/src/controllers/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Query, Body, Res, HttpStatus } from '@nestjs/common'; 2 | import { AppService } from '../services/app.service'; 3 | import errorCode from '../common/errorCode'; 4 | import { Response } from 'express'; 5 | import { validURL } from '../common/stringUtil'; 6 | 7 | @Controller('url') 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Post() 12 | async insertUrl(@Body('longUrl') longUrl: string, @Res() res: Response | any): Promise { 13 | if (!validURL(longUrl)) { 14 | return res.status(HttpStatus.OK).json({ 15 | code: errorCode.URL_NOT_VALID, 16 | message: `invalid url: ${longUrl}`, 17 | }); 18 | } 19 | 20 | const url = await this.appService.insertUrl(longUrl); 21 | return res.status(HttpStatus.OK).json({ 22 | data: url, 23 | code: errorCode.SUCDESS, 24 | }); 25 | } 26 | 27 | @Get() 28 | async findUrl(@Query('shortUrl') shortUrl: string, @Res() res: Response | any): Promise { 29 | if (!validURL(shortUrl)) { 30 | return res.status(HttpStatus.OK).json({ 31 | code: errorCode.URL_NOT_VALID, 32 | }); 33 | } 34 | 35 | const longUrl = await this.appService.findUrl(shortUrl); 36 | if (!longUrl) { 37 | return res.status(HttpStatus.OK).json({ 38 | code: errorCode.URL_NOT_EXIST, 39 | message: `failed to find url: ${shortUrl}`, 40 | }); 41 | } 42 | 43 | return res.status(HttpStatus.OK).json({ 44 | data: longUrl, 45 | code: errorCode.SUCDESS, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /short-url-server/src/entities/url.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Url { 5 | constructor(shortUrl: string, longUrl: string) { 6 | this.shortUrl = shortUrl; 7 | this.longUrl = longUrl; 8 | } 9 | 10 | @PrimaryColumn('varchar', { nullable: false }) 11 | public shortUrl: string; 12 | 13 | @Column('text', { nullable: false }) 14 | public longUrl: string; 15 | } 16 | -------------------------------------------------------------------------------- /short-url-server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import 'reflect-metadata'; 3 | import { createConnection } from 'typeorm'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await createConnection(); 9 | app.enableCors(); 10 | await app.listen(3000); 11 | } 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /short-url-server/src/services/__tests__/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from 'typeorm'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { AppService } from '../app.service'; 4 | 5 | describe('AppController', () => { 6 | let appService: AppService; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | providers: [AppService], 11 | }).compile(); 12 | 13 | appService = app.get(AppService); 14 | await createConnection(); 15 | }); 16 | 17 | describe('root', () => { 18 | it('insert and get url', async () => { 19 | const longUrl = `www.baidu.com/looooooooooooooogurl${Date.now()}`; 20 | const shortUrl = await appService.insertUrl(longUrl); 21 | expect(shortUrl).not.toBeNull(); 22 | 23 | const theLongUrl = await appService.findUrl(shortUrl); 24 | expect(theLongUrl).toBe(longUrl); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /short-url-server/src/services/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { getRepository } from 'typeorm'; 3 | import { Url } from '../entities/url'; 4 | import { string10to62 } from '../common/stringUtil'; 5 | import config from '../common/config'; 6 | 7 | @Injectable() 8 | export class AppService { 9 | async insertUrl(longUrl: string): Promise { 10 | const repository = getRepository(Url); 11 | const existedUrl = await repository.findOne({ longUrl }); 12 | if (existedUrl) { 13 | return existedUrl.shortUrl; 14 | } 15 | 16 | const count = await repository.count(); 17 | const shortUrl = `${config.SERVER_URL}/${string10to62(count)}`; 18 | const url = new Url(shortUrl, longUrl); 19 | url.longUrl = longUrl; 20 | await repository.save(url); 21 | return shortUrl; 22 | } 23 | 24 | async findUrl(shortUrl: string): Promise { 25 | const repository = getRepository(Url); 26 | const url = await repository.find({ shortUrl }); 27 | if (url && url.length > 0) { 28 | return url[0].longUrl; 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /short-url-server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /short-url-server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /short-url-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /short-url-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6" 6 | ], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "outDir": "./build", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true 14 | } 15 | } -------------------------------------------------------------------------------- /short-url-server/tslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:latest", 5 | "tslint-config-prettier" 6 | ], 7 | "jsRules": {}, 8 | "rules": { 9 | "object-literal-sort-keys": false, 10 | "no-implicit-dependencies": false, 11 | "no-this-assignment": false, 12 | "no-var-requires": false, 13 | "max-classes-per-file": false, 14 | "no-submodule-imports": false 15 | }, 16 | "rulesDirectory": [] 17 | } -------------------------------------------------------------------------------- /short-url-web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /short-url-web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@react-native-community', 'plugin:jest/recommended'], 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | rules: { 7 | 'prettier/prettier': [ 8 | 'error', 9 | { 10 | endOfLine: 'auto', 11 | }, 12 | ], 13 | 'max-len': ['warn', { code: 160 }], 14 | 'no-shadow': 0, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /short-url-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /short-url-web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | endOfLine: "auto", 7 | printWidth: 120 8 | }; 9 | -------------------------------------------------------------------------------- /short-url-web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "logo192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "logo512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } -------------------------------------------------------------------------------- /short-url-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "short-url-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "webpack-dev-server --open --mode development", 7 | "build": "webpack --mode production", 8 | "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", 9 | "eslint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", 10 | "test": "react-scripts test", 11 | "eject": "react-scripts eject" 12 | }, 13 | "dependencies": { 14 | "@testing-library/jest-dom": "^4.2.4", 15 | "@testing-library/react": "^9.3.2", 16 | "@testing-library/user-event": "^7.1.2", 17 | "@types/jest": "^24.0.0", 18 | "@types/node": "^12.0.0", 19 | "@types/react": "^16.9.0", 20 | "@types/react-dom": "^16.9.0", 21 | "antd": "^4.4.3", 22 | "axios": "^0.21.1", 23 | "less": "^3.12.2", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "react-scripts": "3.4.1" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.10.5", 45 | "@babel/preset-env": "^7.10.4", 46 | "@babel/preset-react": "^7.10.4", 47 | "babel-loader": "^8.1.0", 48 | "css-loader": "^3.6.0", 49 | "@types/jest": "25.2.3", 50 | "extract-text-webpack-plugin": "^3.0.2", 51 | "file-loader": "^6.0.0", 52 | "html-loader": "^1.1.0", 53 | "html-webpack-plugin": "^4.3.0", 54 | "less-loader": "^6.2.0", 55 | "ts-loader": "^8.0.1", 56 | "jest": "26.0.1", 57 | "typescript": "^3.7.5", 58 | "webpack": "^4.43.0", 59 | "webpack-cli": "^3.3.12", 60 | "webpack-dev-server": "^3.11.0", 61 | "prettier": "^1.19.1", 62 | "tslint-config-prettier": "^1.18.0", 63 | "eslint-plugin-jest": "^24.1.3", 64 | "@react-native-community/eslint-config": "^1.1.0", 65 | "@typescript-eslint/eslint-plugin": "^2.27.0", 66 | "@typescript-eslint/parser": "^2.27.0", 67 | "eslint": "^7.1.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /short-url-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ygweric/short-url/26ac97cdb4d9e6bea76f36f5a138ff8bc554e055/short-url-web/public/favicon.ico -------------------------------------------------------------------------------- /short-url-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | React App 12 | 13 | 14 | 15 |
16 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /short-url-web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ygweric/short-url/26ac97cdb4d9e6bea76f36f5a138ff8bc554e055/short-url-web/public/logo192.png -------------------------------------------------------------------------------- /short-url-web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ygweric/short-url/26ac97cdb4d9e6bea76f36f5a138ff8bc554e055/short-url-web/public/logo512.png -------------------------------------------------------------------------------- /short-url-web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UrlSearch from './page'; 3 | 4 | function App(): any { 5 | return ; 6 | } 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /short-url-web/src/common/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | SERVER_URL: 'http://localhost:3000', 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /short-url-web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'antd/dist/antd.css'; 4 | import './style/css/index.css'; 5 | import App from './App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | -------------------------------------------------------------------------------- /short-url-web/src/page/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Input, message } from 'antd'; 3 | import request from './request'; 4 | const { Search } = Input; 5 | const { insertUrl, findUrl } = request; 6 | 7 | function UrlSearch(): any { 8 | const [result, setResult] = useState(''); 9 | 10 | const handlInsertUrl = useCallback(async value => { 11 | if (!value) { 12 | message.error('请输入链接'); 13 | return; 14 | } 15 | const resp = await insertUrl(value); 16 | const { code, data, message: msg } = resp; 17 | if (code) { 18 | message.error(msg); 19 | } 20 | setResult(`短连接: ${data}`); 21 | }, []); 22 | 23 | const handlFindUrl = useCallback(async value => { 24 | if (!value) { 25 | message.error('请输入短连接'); 26 | return; 27 | } 28 | const resp = await findUrl(value); 29 | const { code, data, message: msg } = resp; 30 | if (code) { 31 | message.error(msg); 32 | } 33 | setResult(`长连接: ${data}`); 34 | }, []); 35 | 36 | return ( 37 |
38 |
{'短连接生成系统'}
39 |
40 |
41 | {'链接 -> 短连接'} 42 | 43 |
44 |
45 | {'短连接 -> 链接'} 46 | 47 |
48 |
49 |
{result}
50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | export default UrlSearch; 57 | -------------------------------------------------------------------------------- /short-url-web/src/page/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../common/config'; 3 | const { SERVER_URL } = config; 4 | 5 | const findUrl = async (shortUrl: string): Promise => { 6 | const url = `${SERVER_URL}/url?shortUrl=${shortUrl}`; 7 | const resp = await axios.get(url); 8 | return resp && resp.data; 9 | }; 10 | 11 | const insertUrl = async (longUrl: string): Promise => { 12 | const url = `${SERVER_URL}/url`; 13 | const body = { longUrl }; 14 | const resp = await axios.post(url, body); 15 | return resp && resp.data; 16 | }; 17 | 18 | export default { 19 | findUrl, 20 | insertUrl, 21 | }; 22 | -------------------------------------------------------------------------------- /short-url-web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | -------------------------------------------------------------------------------- /short-url-web/src/style/css/index.css: -------------------------------------------------------------------------------- 1 | 2 | .search-header{ 3 | width: 100%; 4 | font-size: 3rem; 5 | font-weight: 600; 6 | text-align: center; 7 | margin-top: 2rem; 8 | } 9 | 10 | .search-body-con{ 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | .search-body{ 16 | background-color: #f9f9f9; 17 | width: 70%; 18 | min-width: 20rem; 19 | margin-top: 5rem; 20 | height: 20rem; 21 | 22 | padding: 1rem; 23 | } -------------------------------------------------------------------------------- /short-url-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "outDir": "./dist/", 20 | "noImplicitAny": true, 21 | "jsx": "react", 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } -------------------------------------------------------------------------------- /short-url-web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './src/index.tsx', 6 | devServer: { 7 | contentBase: './dist', 8 | hot: true, 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: 'babel-loader', 17 | }, 18 | }, 19 | { 20 | test: /\.html$/, 21 | use: [ 22 | { 23 | loader: 'html-loader', 24 | }, 25 | ], 26 | }, 27 | { 28 | test: /\.ts|tsx?$/, 29 | use: 'ts-loader', 30 | exclude: /node_modules/, 31 | }, 32 | { 33 | test: /\.css$/i, 34 | use: ['style-loader', 'css-loader'], 35 | }, 36 | { 37 | test: /\.(png|svg|jpg|gif)$/, 38 | use: ['file-loader'], 39 | }, 40 | ], 41 | }, 42 | resolve: { 43 | extensions: ['.tsx', '.ts', '.js'], 44 | }, 45 | output: { 46 | filename: 'bundle.js', 47 | path: path.resolve(__dirname, 'dist'), 48 | }, 49 | plugins: [ 50 | new HtmlWebPackPlugin({ 51 | template: './public/index.html', 52 | filename: './index.html', 53 | }), 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /short-url.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "short-url-server" 5 | }, 6 | { 7 | "path": "short-url-web" 8 | } 9 | ], 10 | "settings": {} 11 | } --------------------------------------------------------------------------------