Eggjs学习系列(一) 使用TypeScript快速入门
Eggjs是一个node的渐近式开发框架,用于服务端开发。而 TypeScript 是 JavaScript的超集,在兼容 JavaScript的基础上增加了类型检查、智能提示等特性,适用于大规模的企业项目开发。下面是Eggjs在 TypeScript 下的基本实践案例:
快速入门
使用 TypeScript 初始化项目
npm init egg --type=ts
npm i
npm run dev
骨架会生成一个极简版的示例方便我进一步深入开发。
编写Controller
通过 Controller 和 Router 来实现路由的跳转和页面的显示
import { Controller } from 'egg';
export default class HomeController extends Controller {
public async index() {
const { ctx } = this;
ctx.body = 'hellow world';
}
}
配置路由映射:
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/', controller.home.index);
};
修改基本配置
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
config.keys = appInfo.name + 'xxxxxxxxxxxxxx_1696';
config.middleware = [];
const bizConfig = {
sourceUrl: `https://github.com/eggjs/examples/tree/master/${appInfo.name}`,
};
return {
...config,
...bizConfig,
};
};
其中 PowerPartial
是递归遍历类中的属性,把类中所有属性都改为可选。
export type PowerPartial<T> = {
[U in keyof T]?: T[U] extends object
? PowerPartial<T[U]>
: T[U]
};
在命令行使用 npm run dev
执行命令,之后打开http://127.0.0.1:7001/
页面,会在页面上显示出 hello world
静态资源
Egg 内置了 static 插件,线上环境建议部署到 CDN,无需该插件。
static 插件默认映射 /public/* -> app/public/*
目录
此处,我们把静态资源都放到 app/public
目录即可:
app/public
├── css
│ └── news.css
└── js
├── lib.js
└── news.js
模板渲染
绝大多数情况,我们都需要读取数据后渲染模板,然后呈现给用户。故我们需要引入对应的模板引擎。框架并不强制你使用某种模板引擎,开发者可以引入不同的插件来实现差异化定制。
例如使用 Nunjucks 来渲染,需要先安装对应插件
$ npm i egg-view-nunjucks --save
开启插件
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
nunjucks: {
enable: true,
package: 'egg-view-nunjucks',
},
};
export default plugin;
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
config.keys = appInfo.name + '_1586659496760_1696';
config.view = {
defaultViewEngine: 'nunjucks',
mapping: {
'.tpl': 'nunjucks',
},
};
};
为列表页编写模板文件,一般放置在 app/view
目录下
<html>
<head>
<title>Hacker News</title>
<link rel="stylesheet" href="/public/css/news.css" />
</head>
<body>
<ul class="news-view view">
{% for item in list %}
<li class="item">
<a href="{{ item.url }}">{{ item.title }}</a>
</li>
{% endfor %}
</ul>
</body>
</html>
添加相应的 Controller 和 Router
import { Controller } from 'egg';
export default class NewsController extends Controller {
public async list() {
const dataList = {
list: [
{ id: 1, title: 'this is news 1', url: '/news/1' },
{ id: 2, title: 'this is news 2', url: '/news/2' },
],
};
await this.ctx.render('news/list.tpl', dataList);
}
}
export default (app: Application) => {
const { controller, router } = app;
router.get('/', controller.home.index);
router.get('/news', controller.news.list);
};
这个时候在 router.ts 中使用 controller
时,代码并没有智能提示新创建的 news
,这是因为没有在 typings/app/controller/index.d.ts
中给 IController
接口绑定新的 Controller。这时,我们可以手动给 IController
添加,也可以使用 Eggjs 提供的 egg-ts-helper
自动生成配置。
最后,启动浏览器,访问 http://localhost:7001/news 即可看到渲染后的页面。
egg-ts-helper
使用方法
安装egg-ts-helper
npm i egg-ts-helper --save-dev
只够修改 package.json
配置,让 egg-ts-helper
能够在开发期间自动生成对应的 d.ts
{
"egg": {
"declarations": true
},
"scripts": {
"dev": "egg-bin dev",
"test-local": "egg-bin test",
"clean": "ets clean"
}
}
之后,使用 npm run dev
运行项目,便会在开发期间自动生成d.ts
配置。 生成后配置文件的内容是
import 'egg';
import ExportHome from '../../../app/controller/home';
import ExportNews from '../../../app/controller/news';
declare module 'egg' {
interface IController {
home: ExportHome;
news: ExportNews;
}
}
这样,在 router
中使用 controller
使,便会给出 news
提示。
编写 service
在实际应用中,Controller 一般不会自己产出数据,也不会包含复杂的逻辑,复杂的过程应抽象为业务逻辑层 Service。添加一个 Service 抓取数据 ,如下:
import { Service } from 'egg';
export default class NewsService extends Service {
public async list(page = 1) {
const { serverUrl, pageSize } = this.config;
const { data: idList } = await this.ctx.curl(`${serverUrl}/topstories.json`, {
timeout: 300000,
data: {
orderBy: '"$key"',
startAt: `"${pageSize * (page - 1)}"`,
endAt: `"${pageSize * page - 1}"`,
},
dataType: 'json',
});
const newsList = await Promise.all(
Object.keys(idList).map(key => {
const url = `${serverUrl}/item/${idList[key]}.json`;
return this.ctx.curl(url, { dataType: 'json' });
}),
);
return newsList.map(res => res.data);
}
}
然后稍微修改下之前的 Controller:
export default class NewsController extends Controller {
public async list() {
const ctx = this.ctx;
const page = ctx.query.page || 1;
const newsList = await ctx.service.news.list(page);
await this.ctx.render('news/list.tpl', { list: newsList });
}
}
最后,添加请求网址的配置
export default (appInfo: EggAppInfo) => {
const news = {
pageSize: 5,
serverUrl: 'https://hacker-news.firebaseio.com/v0',
};
return {
...news,
};
};
编写扩展
扩展内编写一些常用的函数,用于加快开发。
这里,使用 View 插件支持的 Helper 来实现:
npm i moment --save
import moment = require('moment');
export default {
relativeTime(time: number) {
return moment(new Date(time)).fromNow();
},
};
在模板里面使用:
{{ helper.relativeTime(item.time) }}
编写 Middleware
通过编写中间件,来扩展请求的处理。假设有个需求:我们的新闻站点,禁止百度爬虫访问。这种需求就可以通过 Middleware 实现
import { Context, Options } from 'egg';
export default function robotMiddleware(options: Options): any {
return async (ctx: Context, next: () => Promise<any>) => {
const source = ctx.get('user-agent') || '';
const match = options.ua.some(ua => ua.test(source));
if (match) {
ctx.status = 403;
ctx.message = 'Go away, robot.';
}
await next();
};
}
在 Config 添加配置
export default (appInfo: EggAppInfo) => {
config.middleware = [
'robot',
];
const bizConfig = {
robot: {
ua: [
/Baiduspider/i,
],
},
};
const news = {
pageSize: 5,
serverUrl: 'https://hacker-news.firebaseio.com/v0',
};
return {
...config,
...bizConfig,
...news,
};
}
最后,为了使用 TypeScript 的 只能提示,在 typings 中给egg添加 Options 类型
import 'egg';
declare module 'egg' {
interface Options {
ua: Array<RegExp>
}
}
注意:Middleware 目前返回值必须都是 any
,否则使用 route.get/all 等方法的时候因为 Koa 的 IRouteContext
和 Egg 自身的 Context
不兼容导致编译报错。
现在可以使用 curl http://localhost:7001/news -A "Baiduspider"
看看效果。
配置文件
框架提供了强大的配置合并管理功能:
- 支持按环境变量加载不同的配置文件,如
config.local.js
, config.prod.js
等等。 - 应用/插件/框架都可以配置自己的配置文件,框架将按顺序合并加载。
- 配置在使用的时候支持多级提示,并自动关联
import { EggAppInfo, EggAppConfig, PowerPartial } from 'egg';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
config.keys = appInfo.name + '123456';
config.view = {
defaultViewEngine: 'nunjucks',
mapping: {
'.tpl': 'nunjucks',
},
};
const bizConfig = {};
bizConfig.news = {
pageSize: 30,
serverUrl: 'https://hacker-news.firebaseio.com/v0',
};
return {
...config as {},
...bizConfig,
};
};
当 EggAppConfig
合并 config.default.ts
的类型后,在其他 config.{env}.ts
中这么写就也可以获得在 config.default.ts
定义的自定义配置的智能提示:
import { EggAppConfig, } from 'egg';
export default () => {
const config = {} as PowerPartial<EggAppConfig>;
config.news = {
pageSize: 20,
};
return config;
};
单元测试
单元测试用于测试代码功能是否完善,并保证不同版本情况下代码执行结果的一致性。
测试文件应该放在项目根目录下的 test 目录下,并以 test.js
为后缀名,即 {app_root}/test/**/*.test.ts
。
import { app } from 'egg-mock/bootstrap';
describe('test/app/middleware/robot.test.js', () => {
it('should block robot', () => {
return app.httpRequest()
.get('/')
.set('User-Agent', 'Baiduspider')
.expect(403);
});
});
执行测试
npm test
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)