AdonisJS 入门篇

参考资料

前言

本篇文章仅对 AdonisJS 框架作简要介绍及简单语法提及,详细语法跟配置请参考官方文档。本篇的目的在于看完之后能够大体了解 AdonisJS 的架构,并实现简单的功能。

零、简单介绍及安装

1.Introduce

  • AdonisJS 官方介绍是 MVC Framework for Node.js to write webapps with less code,简单的说就是一个 服务端渲染 的 MVC 框架。如果熟悉 Laravel 框架(PHP 框架),你会发现这是 Laravel 的一个 NodeJS 版本。
  • 优点:AdonisJS 作为一个服务端渲染的框架,能够解决很多现在流行的前端框架无法实行的问题(比如兼容 IE8 等)

2.Install

  • 既然 AdonisJS 是依赖于 NodeJS,那么安装自然离不开 NodeJS 和 npm。官方指定版本(当然,官方也建议 npm 版本 >= 6.1.x):
node 版本
npm 版本
  • 创建 Adonis project 很简单,因为 Adonis 本身就是一个 npm package,可以运行以下代码启动一个示例 project:
npm i -g adonis-cli           // 全局安装 adois
adonis new awesome-project    // 当前目录下创建新工程
cd awesome-project    
// 然后你需要把 .env-example 文件改名为 .env
npm run serve:dev    // 启动 project

3.Directory structure

文件夹结构图

一、应用生命周期

1.Http Server

  • Providers 和它们的参数被定义在 bootstrap/app.js
  • 自定义 Provider 在 providers/, provider 文件设置注册信息(注册为异步注册 ES2015 Generator)及依赖的模块
  • 设置参数是为了方便引用 Provider 时书写
// bootstrap/app.js
const provider = []
const aceProviders = []
const aliases = []
const commads = []
  • bootstrap/http.js 文件引用 Providers 并注册在 IoC container
// bootstrap/http.js

// 引用 node_modules 提供的 providers
const app = require('./app')
const fold = require('adonis-fold')
const path = require('path')
const packageFile = path.join(__dirname, '../package.json')
require('./extend')
// 注册
module.exports = function (callback) {
    ...
}
  • 注册定义在 package.json 自动加载路径
// 可以在此配置
{
    "autoload": {
        "App": "./app"
    }
}
  • 根据 bootstrap/http.js 定义的结果继续加载以下文件:

  • bootstrap/event.js: 注册监听事件

  • app/Http/kernel.js: Http server & 中间件

  • app/Http/routes.js: Http server & 路由

  • database/factory.js: Model 管理

  • 最后,启动 HTTP server 监听定义在 .env 文件的 hostport

  • 启动 HTTP server 之前,AdonisJS 提供了 Http.onStart 钩子,可用来自定义全局变量,如 View 的 全局变量过滤器 等。

2.Http Request

  • HTTP request 是在给定时间处理一个或多个 HTTP请求的 动态流

  • 根据 public 文件夹里边的静态文件检查传入的请求 URL ,如果存在则提供静态文件。

  • 接下来,将根据请求的 URL ,搜索相应的路由,并且执行以下步骤:

  • 启动所有全局中间件

  • 启动所有路由中间件

  • 执行路由操作(可以是 控制器方法 或者 闭包

  • 如果以上两个步骤出错,将会抛出一个 404 HttpException。可以在 APP/httP.js 设置 error 时的 sendView(),以提高用户体验。

二、MVC 设计模式

本部分仅对 MVC 模式作简要介绍,后续将进一步介绍 View 和 Controller。

1.概述

  • 根据上节生命周期的说明,我们大致可以知道整个架构的数据流程如果下图所示:
MVC 设计模式示意图
  • 这便是我们常说的 MVC 设计模式。MVC 模式将应用分离成模型(Model),视图(View)和控制器(Controller)。AdonisJS 支持这三者且使其结合起来非常简单。而且,Adonis Router 也在处理 HTTP 请求并将其传递给控制器中起到重要作用。

2.Model

  • Model 是从数据库(AdonisJS 使用的是 SQL)获取数据的数据层,为了使获取数据的过程简单和安全,Adonis 使用了很好的 ORM 架构(Lucid)。

3.Controller

  • 控制器主要控制 HTTP 请求数据流,利用 Model 获取必要的数据(不仅数据库,第三方服务器的数据等都在控制器里边处理),并把获取的数据传递给 View 去渲染 HTML 页面。

4.View

  • View 是整个 MVC 模式数据流的最后部分,主要是利用动态的数据(Nunjuck Template)去渲染(jinja 引擎) HTML 页面。

三、Rounting

本节简单介绍路由,以及常用的几个语法

  • 根据上节的数据流程所讲,接下来我们先介绍路由。路由的作用便是将浏览器的 HTTP 请求传递到相应的控制器中。当然,你也可以直接在路由部分作简单的逻辑处理,不过在这里比较建议将逻辑处理划分到 控制器 中,方便代码的管理。

  • 路由部分的代码书写在 app/Http/routes.js 中,简单的例子如下:

const Route = use('Route')    // 类似 include,跟 require 不同

Route.get('/', function * (request, response) {
    yield response.sendView('home')    // 这里使用 ES6 generator 语法达到异步请求数据的效果
})
  • 路由参数:路由参数是 URL 中的动态数据段,我们可以通过自定义的 URL 接受动态数据。如下例子:
Route.get('users/:id', function * (request, response) {
    const id = request.param('id')
    response.send('Profile for user with id ${id}')
})
  • 路由群:通常,会存在一系列路由共享同一个模块的情况,这就可能有时候需要修改这个模块的一些共同参数,我们把这些路由“集合”到一起,就方便修改了。例如下面的例子:
Route.group('version1', function () {
    Route.get('users', function * (request, response) {  // 路由是 /api/v1/users
        // ...
    })
}).prefix('api/v1').middleware('auth')    // 该路由群都会加上 api/v1 的前缀,并经过用户登录中间件的检验
  • 路由命名:书写完整的路径有时过于麻烦,为方便使用可以给路由命名。如下例子:
Route
    .get('users/:id', '与否.show')
    .as('profile') 

四、Controller

本节主要作控制器的简要介绍,以及 request 和 response 的常用语法使用。最后简单说说中间件。

1.概述

  • 控制器的作用正如前面所说,就是用来处理数据并传递数据给 View 渲染 HTML 页面。处理的数据来源有来自浏览器的 HTTP 请求,也可以从数据库获取,也包括从第三方服务器请求的数据(需自行搭建服务器客户端环境)。控制器的代码结构大致如下:
// 引入依赖文件
const path = require('path')

// 定义控制器
class UsersController {

  // 依赖注入
  static get inject () {
    return ['App/Model/User']   
  }

  // 以参数形式接收依赖注入
  constructor (User) { 
    this.User = User 
  }
  
  // 自定义数据处理方法
  static _processUserInfo() {
    // ...
  }
  
  // 异步调用的方法
  * index () {
    const users = yield this.User.all()
    // 传递数据给视图渲染
    yield response.sendView('users',{
        userName,
        userInfo
    })
  }

}

module.exports = UserController

2.request

2.1 简单示例

  • AdonisJS 中,为了方便接收 HTTP 请求信息,所有的控制器方法和路由器闭包都会接收一个 Request 实例 。请看一下简单示例:
// 路由闭包中调用 request ,response
const Route = use('Route')

Route.get('post', function * (request, response) {
  const body = request.all()
   
  // cherry picking fields
  const body = request.only('title', 'description', 'categories')
})
// 控制器中调用 request, response
class IndexController {
  * index(requset, response) {
    // ...
  }
}

2.2 常用的 request 方法

  • request.all() :包括所有查询字符串和请求体:
const data = request.all()
// example.html?id=1&name=John 
// <input name="gender" value="male" />
// => {"id":1, "name": "John", "gender": "male" }
  • request.only():跟 request.all() 类似,但是只包含定义的 key 值:
const data = request.only('name', 'email', 'age')
/* returns
{
  'name': '..',
  'email':'..',
  'age':'..'
}
*/
  • request.input(key,[defaultValue]):返回 input 标签对应的键值对,如果 value 为空,则返回 defaultValue
const name = request.input('name')
const subscribe = request.input('subscribe', 'yes')
  • request.url() :获取请求的 URL(出去查询字符串):
// url - http://foo.com/users?orderBy=desc&limit=10
request.url()
// returns - http://foo.com/users
  • request.param(key, [defaultValue]) :返回查询字符串:
// url - http://foo.com/users?orderBy=desc&limit=10
request.param('orderBy')
// returns - desc

// 返回所有的查询字符串
request.params()
  • request.collect(key1, key2, ...):转化数据格式,直接看下面例子:
<form method="POST" action="/users">
  <div class="row">
    <h2> User 1 </h2>
    <input type="email" name="email[]" />
    <input type="password" name="password[]" />
  </div>

  <div class="row">
    <h2> User 2 </h2>
    <input type="email" name="email[]" />
    <input type="password" name="password[]" />
  </div>

  <button type="submit"> Create Users </button>
</form>
request.only('email', 'password', )
/* returns 
{
  email: ['bar@foo.com', 'baz@foo.com'],
  password: ['secret', 'secret1']
}
*/
request.collect('email', 'password')
/* returns
[
  {
    email: 'bar@foo.com',
    password: 'secret'
  },
  {
    email: 'baz@foo.com',
    password: 'secret1'
  }
]
*/

3.response

  • 为了方便尽快响应 HTTP 请求,AdonisJS 提供了 response 类给我们使用。可用于渲染 nunjucks views 或者格式化数据。以下是简单的示例代码:
// Render Views
Route
  .get('/', function *() {
    yield response.sendView('welcome')    // resources/views/welcome.njk
})
// JSON Response
Route
  .get('user', function * () {
    const user = yield User.all()    // fetch users
    response.json(users)
})
  • 常用的还有重定向(Redirects)
response.location('/signup')
// or
response.location('back')

4.middleware

  • 中间件的使用十分常见,比如验证用户是否登录。在 AdonisJS 中,中间件放在 app/Http/Middleware 文件夹下,以下是简单示例:
'use strict'

const geoip = use('geoip-lite')  // npm module

class CountryDetector {
  
  * handle (request, response, next) {
    const ip = request.ip()
    request.country = geoip.lookup(ip).country
    yield next    // 如果想要把 req, res 传递给下个中间件或者路由行为,注意使用 yield next 
  }
}

五、View

本节简要介绍 View 层以及 View 层常见的使用。

1.概述

  • 正如前边的数据流图所述,View 便是整个数据流程的末端,即利用前边步骤传来的数据进行页面渲染。AdonisJS 的视图引擎是建立在 nunjucks 的基础上的,所以 nunjucks 有的模板语法,在 AdonisJS 中一样可以使用。另外,AdonisJS 也提供了一些特有的 过滤器(filter)

  • 简单示例:

// Route
const Route = use('Route')
Route.get('/greet/:user', 'UserController.greet')

// Controller
class UserController {
  * greet (request, response) {
     const user = request.param('user')  // user: "John"
     yield response.sendView('greet', {user})
  }
}
// View
<h2>Hello {{ user }}</h2>    // <h2>John</h2>

2.常见使用

  • 条件语法
{% if hungry %}
  I am hungry
{% else %}
  I am good
{% endif %}
{{ username | capitalize }}
// username = 'john' => output = John
  • 遍历语法
{% for book in books %}
  book
{% endfor %}
  • 模板继承
// resources/views/master.njk
<html>
  <body>

    <header class="header">
      {% block header %}
        Common Header
      {% endblock %}
    </header>

    <section class="sidebar">
      {% block sidebar %}
        Common Sidebar
      {% endblock %}
    </section>

    <section class="content">
      {% block content %}{% endblock %}
    </section>

  </body>
</html>
// resources/views/home.njk
{% extends 'master' %}

{% block content %}
  Here comes the content of the home page.
{% endblock %}
<html>
  <body>

    <header class="header">
      Common Header
    </header>

    <section class="sidebar">
      Common Sidebar
    </section>

    <section class="content">
      Here comes the content of the home page.
    </section>

  </body>
</html>
  • 引入
// resources/views/chat/message.njk
<div class="chat__message">
  <h2> {{ message.from }} </h2>
  <p> {{ message.body }} </p>
</div>
// resources/views/chat/index.njk
{% for message in messages %}
  {% include 'message' %}
{% endfor %}
  • macro & import
// resource/views/macros/button.njk
{% macro button(value, style='default') %}
  <button type="button" class="btn btn-{{style}}"> {{ value }} </button>
{% endmacro %}
// resources/views/home.njk
{% from 'macros.button' import button %}
{{ button('Create User', 'primary') }}

3.自定义全局变量和过滤器

  • app/Listeners/Http.js 中,提供了 Http.onStart 函数钩子,供我们定义全局变量和过滤器。下面是两个简单的例子:
// 定义全局变量
Http.onStart = function () {
  const View = use('View')
  View.global('time', function () {
    return new Date().getTime()
  })
}
// 定义过滤器
const View = use('Adonis/Src/View')
const accounting = use('accounting') // npm module

View.filter('currency', function (amount, symbol) {
  return accounting.formatMoney(amount, {symbol})
})

// 使用过滤器
{{ 1000 | currency('$') }}

{# returns $1,000.00 #}

六、IoC Container & Service Providers

  • Ioc Container 主要概念就是在容器内存储/绑定依赖关系,然后从容器中取回它们,而不是手动请求它们。这种方法主要有以下三种好处:

  • 对 End-user 隐藏对象的配置,提供简单明了的 API

  • 坚固支持依赖注入(DI),因为所有对象都是从单一的真实来源获取的

  • 易于编写第三方模块/插件,因为你可以从 IoC 容器获取依赖关系,而不是让 End-user 手动传递它们。

  • 绑定依赖关系示例代码如下:

const Ioc = require('adonis-fold').Ioc
const bugsnag = require('bugsnag')

Ioc.bind('Adonis/Src/Bugsnag', function (app) { 

  const Config = app.use('Adonis/Src/Config') 
  const bugSnagConfig = Config.get('services.bugsnag') 

  bugsnag.register(bugSnagConfig.apiKey, bugSnagConfig.options) 
  return bugsnag 

})
  • 绑定 Service Provider 的示例代码如下:
const ServiceProvider = require('adonis-fold').ServiceProvider

class BugSnagProvider extends ServiceProvider {

  * register () { 
    this.app.bind('Adonis/Addons/BugSnag', (app) => {
      const BugSnag = require('./BugSnag')
      const Config = app.use('Adonis/Src/Config')
      return new BugSnag(Config)
    })
  }

  * boot () { 
    // Everything is registered do some hard work
  }

}

注:本篇对 model 部分的简单略过。相关配置,Ace 开发工具等请参考官方文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容