[Kails]一个基于 Koa2 构建的类似于 Rails 的 nodejs 开源项目 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
embbnux
V2EX    Node.js

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

  •  
  •   embbnux 2016-09-05 12:19:40 +08:00 4757 次点击
    这是一个创建于 3323 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近研究了下 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 简化部署
    • 线上代码预编译
    6 条回复    2016-11-04 23:27:47 +08:00
    Niphor
        1
    Niphor  
       2016-09-05 14:25:52 +08:00
    楼主 你这个同构...
    server 端不是用的 pug 么...
    embbnux
        2
    embbnux  
    OP
       2016-09-05 14:31:21 +08:00
    @Niphor 这一块还没做的满意,现在是 react seerver 渲染 react 部分成 string ,传给 pug
    embbnux
        3
    embbnux  
    OP
       2016-09-05 21:41:45 +08:00
    线上环境是这个: https://kails.org/
    Tmac15
        4
    Tmac15  
       2016-09-07 10:08:35 +08:00 via iPhone
    楼主的 kails 跟 rails 很像,超赞
    zhfish
        5
    zhfish  
       2016-10-07 19:55:05 +08:00
    node 估计快 7.0 LTS 了, 所以 async/await 可以直接尝试用 node6.x 了, v8 已支持,既然是新东西,离生产还有段距离,不如潮一点, 4.x 和 6.x 特性差的越来越多了
    embbnux
        6
    embbnux  
    OP
       2016-11-04 23:27:47 +08:00
    @zhfish 是啊直接换 7, 不过 7 应该不会是 lts 只有偶数会
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2661 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 02:45 PVG 10:45 LAX 19:45 JFK 22:45
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86