[Kails] 一个基于 Koa2 构建的类似于 Rails 的 nodejs 开源项目

最近研究了下Koa2框架,喜爱其中间件的思想。但是发现实在是太简洁了,只有基本功能,虽然可以方便搭各种服务,但是离可以适应快速开发的网站框架还是有点距离。于是参考Rails的大致框架搭建了个网站框架kails, 配合postgres和redis, 实现了MVC架构,前端webpack,react前后端同构等网站开发基本框架。本文主要介绍kails搭建中的各种技术栈和思想。

本文首发于Blog of Embbnux, 转载请注明原文出处:

https://www.embbnux.com/2016/09/04/kails_with_koa2_like_ruby_on_rails/

koa来源于express的主创团队,主要利用es6的generators特性实现了基于中间件思想的新的框架,但是和express不同,koa并不想express一样提供一个可以满足基本网站开发的框架,而更像是一个基本功能模块,要满足网站还是需要自己引入很多功能模块。所以根据选型大的不同,有各种迥异的koa项目,kails由名字也可以看出是一个类似Ruby on Rails的koa项目。

项目地址: https://github.com/embbnux/kails

主要目录结构如下:

├── app.js
├── assets
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── config
│   ├── config.js
│   ├── development.js
│   ├── test.js
│   ├── production.js
│   └── webpack.config.js
│   ├── webpack
├── routes
├── models
├── controllers
├── views
├── db
│   └── migrations
├── helpers
├── index.js
├── package.json
├── public
└── test

一、第一步es6支持

kails选用的是koa2作为核心框架,koa2使用es7的async和await等功能,node在开启harmony后还是不能运行,所以要使用babel等语言转化工具进行支持:

babel6配置文件:

.babelrc:

{
  "presets": [
    "es2015",
    "stage-0",
    "react"
  ]
}

在入口使用babel加载整个功能,使支持es6

require(‘babel-core/register‘)
require(‘babel-polyfill‘)
require(‘./app.js‘)

二、核心文件app.js

app.js是核心文件,koa2的中间件的引入和使用主要在这里,这里会引入各种中间件和配置, 具体详细功能介绍后面会慢慢涉及到。

下面是部分内容,具体内容见github上仓库

import Koa from ‘koa‘
import session from ‘koa-generic-session‘
import csrf from ‘koa-csrf‘
import views from ‘koa-views‘
import convert from ‘koa-convert‘
import json from ‘koa-json‘
import bodyParser from ‘koa-bodyparser‘

import config from ‘./config/config‘
import router from ‘./routes/index‘
import koaRedis from ‘koa-redis‘
import models from ‘./models/index‘

const redisStore = koaRedis({
  url: config.redisUrl
})

const app = new Koa()

app.keys = [config.secretKeyBase]

app.use(convert(session({
  store: redisStore,
  prefix: ‘kails:sess:‘,
  key: ‘kails.sid‘
})))

app.use(bodyParser())
app.use(convert(json()))
app.use(convert(logger()))

// not serve static when deploy
if(config.serveStatic){
  app.use(convert(require(‘koa-static‘)(__dirname + ‘/public‘)))
}

//views with pug
app.use(views(‘./views‘, { extension: ‘pug‘ }))

// csrf
app.use(convert(csrf()))

app.use(router.routes(), router.allowedMethods())

app.listen(config.port)
export default app

三、MVC框架搭建

网站架构还是以mvc分层多见和实用,能满足很多场景的网站开发了,逻辑再复杂点可以再加个服务层,这里基于koa-router进行路由的分发,从而实行MVC分层

路由的配置主要由routes/index.js文件去自动加载其目录下的其它文件,每个文件负责相应的路由头下的路由分发,如下

routes/index.js

import fs from ‘fs‘
import path from ‘path‘
import Router from ‘koa-router‘

const basename = path.basename(module.filename)
const router = Router()

fs
  .readdirSync(__dirname)
  .filter(function(file) {
    return (file.indexOf(‘.‘) !== 0) && (file !== basename) && (file.slice(-3) === ‘.js‘)
  })
  .forEach(function(file) {
    let route = require(path.join(__dirname, file))
    router.use(route.routes(), route.allowedMethods())
  })

export default router

路由文件主要负责把相应的请求分发到对应controller中,路由主要采用restful分格。

routes/articles.js

import Router from ‘koa-router‘
import articles from ‘../controllers/articles‘

const router = Router({
  prefix: ‘/articles‘
})
router.get(‘/new‘, articles.checkLogin, articles.newArticle)
router.get(‘/:id‘, articles.show)
router.put(‘/:id‘, articles.checkLogin, articles.checkArticleOwner, articles.checkParamsBody, articles.update)
router.get(‘/:id/edit‘, articles.checkLogin, articles.checkArticleOwner, articles.edit)
router.post(‘/‘, articles.checkLogin, articles.checkParamsBody, articles.create)

// for require auto in index.js
module.exports = router

model层这里基于Sequelize实现orm对接底层数据库postgres, 利用sequelize-cli实现数据库的迁移功能.

例子:

user.js

import bcrypt from ‘bcrypt‘

export default function(sequelize, DataTypes) {
  const User = sequelize.define(‘User‘, {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: {
      type: DataTypes.STRING,
      validate: {
        notEmpty: true,
        len: [1, 50]
      }
    },
    email: {
      type: DataTypes.STRING,
      validate: {
        notEmpty: true,
        isEmail: true
      }
    },
    passwordDigest: {
      type: DataTypes.STRING,
      field: ‘password_digest‘,
      validate: {
        notEmpty: true,
        len: [8, 128]
      }
    },
    password: {
      type: DataTypes.VIRTUAL,
      allowNull: false,
      validate: {
        notEmpty: true
      }
    },
    passwordConfirmation: {
      type: DataTypes.VIRTUAL
    }
  },{
    underscored: true,
    tableName: ‘users‘,
    indexes: [{ unique: true, fields: [‘email‘] }],
    classMethods: {
      associate: function(models) {
        User.hasMany(models.Article, { foreignKey: ‘user_id‘ })
      }
    },
    instanceMethods: {
      authenticate: function(value) {
        if (bcrypt.compareSync(value, this.passwordDigest)){
          return this
        }
        else{
          return false
        }
      }
    }
  })
  function hasSecurePassword(user, options, callback) {
    if (user.password != user.passwordConfirmation) {
      throw new Error(‘Password confirmation doesn\‘t match Password‘)
    }
    bcrypt.hash(user.get(‘password‘), 10, function(err, hash) {
      if (err) return callback(err)
      user.set(‘passwordDigest‘, hash)
      return callback(null, options)
    })
  }
  User.beforeCreate(function(user, options, callback) {
    user.email = user.email.toLowerCase()
    if (user.password){
      hasSecurePassword(user, options, callback)
    }
    else{
      return callback(null, options)
    }
  })
  User.beforeUpdate(function(user, options, callback) {
    user.email = user.email.toLowerCase()
    if (user.password){
      hasSecurePassword(user, options, callback)
    }
    else{
      return callback(null, options)
    }
  })
  return User
}

四、开发、测试与线上环境

网站开发测试与部署等都会有不同的环境,也就需要不同的配置,这里我主要分了development,test和production环境,使用时用自动基于NODE_ENV变量加载不同的环境配置。

实现代码:

config/config.js

var _ = require(‘lodash‘);
var development = require(‘./development‘);
var test = require(‘./test‘);
var production = require(‘./production‘);

var env = process.env.NODE_ENV || ‘development‘;
var configs = {
  development: development,
  test: test,
  production: production
};
var defaultConfig = {
  env: env
};

var config = _.merge(defaultConfig, configs[env]);

module.exports = config;

生产环境的配置:

config/production.js

const port = Number.parseInt(process.env.PORT, 10) || 5000
module.exports = {
  port: port,
  hostName: process.env.HOST_NAME_PRO,
  serveStatic: process.env.SERVE_STATIC_PRO || false,
  assetHost: process.env.ASSET_HOST_PRO,
  redisUrl: process.env.REDIS_URL_PRO,
  secretKeyBase: process.env.SECRET_KEY_BASE
};

五、利用中间件优化代码

koa是以中间件思想构建的,自然代码中离不开中间件,这里介绍几个中间件的应用

currentUser的注入:

currentUser用于获取当前登录用户,在网站用户系统上中具有重要的重要

app.use(async (ctx, next) => {
  let currentUser = null
  if(ctx.session.userId){
    currentUser = await models.User.findById(ctx.session.userId)
  }
  ctx.state = {
    currentUser: currentUser,
    isUserSignIn: (currentUser != null)
  }
  await next()
})

这样在以后的中间件中就可以通过ctx.state.currentUser得到当前用户

优化controller代码

比如article的controller里的edit和update,都需要找到当前的article对象,也需要验证权限,而且是一样的,为了避免代码重复,这里也可以用中间件

controllers/articles.js

async function edit(ctx, next) {
  const locals = {
    title: ‘编辑‘,
    nav: ‘article‘
  }
  await ctx.render(‘articles/edit‘, locals)
}

async function update(ctx, next) {
  let article = ctx.state.article
  article = await article.update(ctx.state.articleParams)
  ctx.redirect(‘/articles/‘ + article.id)
  return
}

async function checkLogin(ctx, next) {
  if(!ctx.state.isUserSignIn){
    ctx.status = 302
    ctx.redirect(‘/‘)
    return
  }
  await next()
}

async function checkArticleOwner(ctx, next) {
  const currentUser = ctx.state.currentUser
  const article = await models.Article.findOne({
    where: {
      id: ctx.params.id,
      userId: currentUser.id
    }
  })
  if(article == null){
    ctx.redirect(‘/‘)
    return
  }
  ctx.state.article = article
  await next()
}

在路由中应用中间件

router.put(‘/:id‘, articles.checkLogin, articles.checkArticleOwner, articles.update)
router.get(‘/:id/edit‘, articles.checkLogin, articles.checkArticleOwner, articles.edit)

这样就相当于实现了rails的before_action的功能

六、webpack配置静态资源

在没实现前后端分离前,工程代码中肯定还是少不了前端代码,现在在webpack是前端模块化编程比较出名的工具,这里用它来做rails中assets pipeline的功能,这里介绍下基本的配置。

config/webpack/base.js

var webpack = require(‘webpack‘);
var path = require(‘path‘);
var publicPath = path.resolve(__dirname, ‘../‘, ‘../‘, ‘public‘, ‘assets‘);
var ManifestPlugin = require(‘webpack-manifest-plugin‘);
var assetHost = require(‘../config‘).assetHost;
var ExtractTextPlugin = require(‘extract-text-webpack-plugin‘);

module.exports = {
  context: path.resolve(__dirname, ‘../‘, ‘../‘),
  entry: {
    application: ‘./assets/javascripts/application.js‘,
    articles: ‘./assets/javascripts/articles.js‘,
    editor: ‘./assets/javascripts/editor.js‘
  },
  module: {
    loaders: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: [‘babel-loader‘],
      query: {
        presets: [‘react‘, ‘es2015‘]
      }
    },{
      test: /\.coffee$/,
      exclude: /node_modules/,
      loader: ‘coffee-loader‘
    },
    {
      test: /\.(woff|woff2|eot|ttf|otf)\??.*$/,
      loader: ‘url-loader?limit=8192&name=[name].[ext]‘
    },
    {
      test: /\.(jpe?g|png|gif|svg)\??.*$/,
      loader: ‘url-loader?limit=8192&name=[name].[ext]‘
    },
    {
      test: /\.css$/,
      loader: ExtractTextPlugin.extract("style-loader", "css-loader")
    },
    {
      test: /\.scss$/,
      loader: ExtractTextPlugin.extract(‘style‘, ‘css!sass‘)
    }]
  },
  resolve: {
    extensions: [‘‘, ‘.js‘, ‘.jsx‘, ‘.coffee‘, ‘.json‘]
  },
  output: {
    path: publicPath,
    publicPath: assetHost + ‘/assets/‘,
    filename: ‘[name]_bundle.js‘
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: ‘jquery‘,
      jQuery: ‘jquery‘
    }),
    // new webpack.HotModuleReplacementPlugin(),
    new ManifestPlugin({
      fileName: ‘kails_manifest.json‘
    })
  ]
};

七、react前后端同构

node的好处是v8引擎只要是js就可以跑,所以想react的渲染dom功能也可以在后端渲染,有利用实现react的前后端同构,利于seo,对用户首屏内容也更加友好。

在前端跑react我就不说了,这里讲下在koa里面怎么实现的:

import React from ‘react‘
import { renderToString } from ‘react-dom/server‘
async function index(ctx, next) {
  const prerenderHtml = await renderToString(
    <Articles articles={ articles } />
  )
}

八、测试与lint

测试和lint自然是开发过程中工程化不可缺少的一部分,这里kails的测试采用mocha,lint使用eslint

.eslintrc:

{
  "parser": "babel-eslint",
  "root": true,
  "rules": {
    "new-cap": 0,
    "strict": 0,
    "no-underscore-dangle": 0,
    "no-use-before-define": 1,
    "eol-last": 1,
    "indent": [2, 2, { "SwitchCase": 0 }],
    "quotes": [2, "single"],
    "linebreak-style": [2, "unix"],
    "semi": [1, "never"],
    "no-console": 1,
    "no-unused-vars": [1, {
      "argsIgnorePattern": "_",
      "varsIgnorePattern": "^debug$|^assert$|^withTransaction$"
    }]
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
    "mocha": true
  },
  "extends": "eslint:recommended"
}

九、console

用过rails的,应该都知道rails有个rails console,可以已命令行的形式进入网站的环境,很是方便,这里基于repl实现:

if (process.argv[2] && process.argv[2][0] == ‘c‘) {
  const repl = require(‘repl‘)
  global.models = models
  repl.start({
    prompt: ‘> ‘,
    useGlobal: true
  }).on(‘exit‘, () => { process.exit() })
}
else {
  app.listen(config.port)
}

十、pm2部署

开发完自然是要部署到线上,这里用pm2来管理:

NODE_ENV=production ./node_modules/.bin/pm2 start index.js -i 2 --name "kails" --max-memory-restart 300M --merge-logs --log-date-format="YYYY-MM-DD HH:mm Z" --output="log/production.log"

十一、npm scripts

有些常用命令参数较多,也比较长,可以使用npm scripts里为这些命令做一些别名

{
  "scripts": {
    "console": "node index.js console",
    "start": "./node_modules/.bin/nodemon index.js & node_modules/.bin/webpack --config config/webpack.config.js --progress --colors --watch",
    "app": "node index.js",
    "pm2": "NODE_ENV=production ./node_modules/.bin/pm2 start index.js -i 2 --name \"kails\" --max-memory-restart 300M --merge-logs --log-date-format=\"YYYY-MM-DD HH:mm Z\" --output=\"log/production.log\"",
    "pm2:restart": "NODE_ENV=production ./node_modules/.bin/pm2 restart \"kails\"",
    "pm2:stop": "NODE_ENV=production ./node_modules/.bin/pm2 stop \"kails\"",
    "pm2:monit": "NODE_ENV=production ./node_modules/.bin/pm2 monit \"kails\"",
    "pm2:logs": "NODE_ENV=production ./node_modules/.bin/pm2 logs \"kails\"",
    "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive --harmony --require babel-polyfill",
    "assets_build": "node_modules/.bin/webpack --config config/webpack.config.js",
    "assets_compile": "NODE_ENV=production node_modules/.bin/webpack --config config/webpack.config.js -p",
    "webpack_dev": "node_modules/.bin/webpack --config config/webpack.config.js --progress --colors --watch",
    "lint": "eslint . --ext .js",
    "db:migrate": "node_modules/.bin/sequelize db:migrate",
    "db:rollback": "node_modules/.bin/sequelize db:migrate:undo",
    "create:migration": "node_modules/.bin/sequelize migration:create"
  }
}

这样就会多出这些命令:

npm install
npm run db:migrate
NODE_ENV=test npm run db:migrate
# run for development, it start app and webpack dev server
npm run start
# run the app
npm run app
# run the lint
npm run lint
# run test
npm run test
# deploy
npm run assets_compile
NODE_ENV=production npm run db:migrate
npm run pm2

十二、更进一步

目前kails实现了基本的博客功能,有基本的权限验证,以及markdown编辑等功能.

现在目前能想到更进一步的:

- 性能优化,加快响应速度

- Dockerfile简化部署

- 线上代码预编译

欢迎pull request : https://github.com/embbnux/kails

时间: 2024-11-07 07:18:32

[Kails] 一个基于 Koa2 构建的类似于 Rails 的 nodejs 开源项目的相关文章

让你如绅士般基于描述编写 Python 命令行工具的开源项目:docopt

作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Article 一.前言 在本系列前面四篇文章中,我们介绍了 argparse 的方方面面.它无疑是强大的,但使用方式上略显麻烦.需要先设置解析器,再定义参数,再解析命令行,最后实现业务逻辑. 而今天要介绍的 docopt 则是站在一个全新的视角来审视命令行.你可曾想过,一个命令行程序的帮助信息其实已然包含了这个命令

基于Grunt构建一个JavaScript库

现在公认的JavaScript典型项目需要运行单元测试,合并压缩.有些还会使用代码生成器,代码样式检查或其他构建工具. Grunt.js是一个开源工具,可以帮助你完成上面的所有步骤.它非常容易扩展,并使用JavaScript书写,所以任何为JavaScript库或项目工作的人都可以按自己的需要扩展它. 本文解释如何使用Grunt.js构建JavaScript库.Grunt.js依赖Node.js和npm,所以第一节解释其是什么,如何安装和使用.如果你对npm有了解,那你可以跳过这一节.第四和第五

构建一个基于 Spring 的 RESTful Web Service

本文详细介绍了基于Spring创建一个“hello world” RESTful web service工程的步骤. 目标 构建一个service,接收如下HTTP GET请求: http://localhost:8080/greeting 并返回如下JSON格式的问候语: {"id":1,"content":"Hello, World!"} 你也可以通过指定查询字符串中的可选参数name来定制问候语: http://localhost:8080

如何基于Python构建一个可扩展的运维自动化平台

嘉宾简介 刘天斯 从事互联网运维工作已13年,目前就职于腾讯-互动娱乐部,负责游戏大数据的运营,曾就职于天涯社区,担任首席架构师/系统管理员. 热衷开源技术的研究,包括系统架构.运维开发.负载均衡.缓存技术.数据库.NOSQL.分布式存储.消息中间件.大数据及云计算.Mesos.Docker.DevOps等领域.擅长大规模集群的运维工作,尤其在自动化运维方面有着非常丰富的经验.同时热衷于互联网前沿技术的研究,活跃在国内社区.业界技术大会,充当一名开源技术的传播与分享者. 导言 受 Reboot

利用Dockerfile构建一个基于CentOS 7镜像

利用Dockerfile构建一个基于CentOS 7,包括java 8, tomcat 7,php ,mysql+mycat的镜像. Dockerfile内容如下: FROM centosMAINTAINER Victor [email protected]WORKDIR /rootRUN rm -f /etc/yum.repos.d/*RUN  echo '[BASE]' > /etc/yum.repos.d/base.repoRUN  echo 'name=base' >> /etc

PhantomJS是一个基于WebKit的服务器端JavaScript API

PhantomJS是一个基于WebKit的服务器端JavaScript API,它基于 BSD开源协议发布.PhantomJS无需浏览器的支持即可实现对Web的支持,且原生支持各种Web标准,如DOM 处理.JavaScript.CSS选择器.JSON.Canvas和可缩放矢量图形SVG.PhantomJS主要是通过JavaScript和CoffeeScript控制WebKit的CSS选择器.可缩放矢量图形SVG和HTTP网络等各个模块.PhantomJS主要支持Windows.Mac OS.L

Stimulsoft Reports Designer.Silverlight是一个基于web的报表设计器控件

Stimulsoft Reports Designer.Silverlight是一个基于web的报表设计器控件,通过使用它您可以直接在web浏览器中更改您的报表控件.该产品使用Silverlight技术和ASP.NET开发.它不需要开发人员编写复杂的代码或很长的组件设置.您在服务器上使用的是一个简单的ASP.NET组件.Silverlight组件在客户端上运行.Stimulsoft Reports Designer.Silverlight拥有一个时尚的用户界面,加载迅速,运行速度快,并拥有丰富的

iKcamp|基于Koa2搭建Node.js实战(含视频)? 视图Nunjucks

视频地址:https://www.cctalk.com/v/15114923888328 视图 Nunjucks 彩虹是上帝和人类立的约,上帝不会再用洪水灭人. 客户端和服务端之间相互通信,传递的数据最终都会展示在视图中,这时候就需要用到『模板引擎』. 什么是模板引擎? 模板引擎是为了使用户界面与业务数据分离而产生的,可以生成特定格式的文档.例如,用于网站的模板引擎会生成一个标准的 HTML 文档. 市面上常见的模板引擎很多,例如:Smarty.Jade.Ejs.Nunjucks 等,可以根据个

安全狗Poon:如何基于云端构建安全体系

日前,ArchSummit 2015 全球架构师峰会在深圳举行,知名互联网安全品牌安全狗作为本次大会的重要赞助方重装亮相本次大会并为大家带来题为"如何基于云端构建安全体系"的主题演讲. ArchSummit全球架构师峰会是InfoQ中国团队推出的面向高端技术管理者.架构师的技术大会,参会者中超过50%拥有8年以上的工作经验.本次与安全狗的合作,是其对云安全领域的目光聚焦,也是对当前企业安全需求的很好回应. 安全狗在本次峰会的演讲者是其首席安全架构师程Poon.Poon是一个长期从事WE