Recently studied the Koa2 framework and liked the idea of its middleware. But found it is too simple, only basic functions, although it can be convenient to build a variety of services, but can adapt to the rapid development of the site framework or a little distance. Therefore, a website framework kails was built by referring to the general framework of Rails. In cooperation with Postgres and Redis, the basic framework of website development such as MVC architecture, front-end Webpack, react front and back end ishomogeneous was realized. This article focuses on the various technology stacks and ideas in kails scaffolding. Individual original, all rights reserved, reprint please indicate the source, and keep the original link: www.embbnux.com/2016/09/04/…

Koa comes from express’s generators team and uses the ES6 generators feature to implement a new middleware based framework. However, unlike Express, KOA does not provide a framework for basic web development, but more of a basic functional module. To satisfy the site still need to introduce a lot of functional modules. So depending on the selection, there are a lot of different KOA projects, and Kails is a Ruby on Rails koA project as the name suggests.

Project address: github.com/embbnux/kai… Welcome to the Pull Request

The main directory structure is as follows:

├─ app.js ├─ assets │ ├─ images │ ├── javascripts │ ├── config.js │ ├─ development.js │ ├── ├.js │ ├ ─ ─ test. Js │ ├ ─ ─ production. Js │ └ ─ ─ webpack. Config. Js │ ├ ─ ─ webpack ├ ─ ─ routes ├ ─ ─ models ├ ─ ─ controllers ├ ─ ─ views ├ ─ ─ the db │ └ ─ ─ migrations ├ ─ ─ helpers ├ ─ ─ index. The js ├ ─ ─ package. The json ├ ─ ─ public └ ─ ─ the testCopy the code

Step 1 es6 support

Kails uses KoA2 as its core framework. Koa2 uses ES7 async and await functions. Node still doesn’t run after Harmony is enabled, so you need to use Babel to support it.


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

Use Babel in the entry to load the entire functionality to enable ES6 support


require('babel-core/register')
require('babel-polyfill')
require('./app.js')
Copy the code

2. Core file app.js

App.js is the core file, and the introduction and use of KOA2 middleware are mainly here. Various middleware and configurations will be introduced here, and detailed functions will be introduced later.

Here are some of the details, which can be found in the Github repository

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 appCopy the code

Iii. MVC framework construction

Website architecture or MVC layer is more common and practical, can meet many scenarios of website development, logic more complex point can add a service layer, here based on KOa-Router routing distribution, so as to implement the CONFIGURATION of MVC layer routing mainly by routes/index.js file to automatically load other files under its directory. Each file distributes routes in the route header as follows: 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 routerCopy the code

The routing file is mainly responsible for distributing the corresponding request to the corresponding controller, and the routing mainly adopts restful partition. 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 = routerCopy the code

In the Model layer, ORM is connected to the underlying database Postgres based on Sequelize, and the database migration function is realized by Sequelize – CLI. Example: 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 }Copy the code

Iv. Development, testing and online environment

Website development,test and deployment will have different environments, which requires different configurations. Here I mainly divide the development,test and production environments, and automatically load different environment configurations based on NODE_ENV variable when using. Implementation code: 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;
Copy the code

Production environment configuration: 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
};

Copy the code

Optimize code with middleware

Koa is built with the idea of middleware, which is inseparable from the natural code. Here we introduce the injection of several middleware applications currentUser: currentUser is used to obtain the current login user, which is of great importance in the website user system

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() })Copy the code

Ctx.state.currentuser can be used in middleware to get the currentUser optimization controller code, such as edit and update in article controller, by finding the current article object and verifying permissions. Again, middleware controllers/articles.js can be used to avoid code duplication

Async function edit(CTX, next) {const locals = {title: 'edit ', 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() }Copy the code

Apply middleware to routing


router.put('/:id', articles.checkLogin, articles.checkArticleOwner, articles.update)
router.get('/:id/edit', articles.checkLogin, articles.checkArticleOwner, articles.edit)
Copy the code

This is equivalent to implementing rails’ before_action functionality

Webpack configudes static resources

Before the separation of front and back ends is realized, front-end code must be indispensable in engineering code. Now WebPack is a well-known tool for front-end modular programming, and it is used here to do the functions of Assets Pipeline in Rails. The basic configuration is introduced here. 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' }) ] };Copy the code

React isomorphic front and rear ends

The advantage of Node is that the V8 engine can run as long as it is JS. Therefore, the DOM rendering function of React can also be rendered at the back end. The isomers of the front and back ends of React are implemented, which is conducive to SEO and more user-friendly for the first screen content. React runs on the front end. I’m not going to talk about it. Here’s how it works in koA:


import React from 'react'
import { renderToString } from 'react-dom/server'
async function index(ctx, next) {
  const prerenderHtml = await renderToString(
    
  )
}
Copy the code

Tests and Lint

Testing and Lint are naturally an integral part of the engineering process. Here Kails tests use Mocha and Lint uses 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"
}
Copy the code

Nine, the console

For those of you who have used Rails, you probably know that Rails has a Rails Console, which allows you to access the site’s environment from the command line. This is implemented based on the 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)
}
Copy the code

X. PM2 deployment

After development, it is natural to deploy online, which is managed by 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"
Copy the code

NPM Scripts

Some common commands have long parameters. You can use NPM scripts to alias these commands

{ "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" } }Copy the code

This would add these commands:


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
Copy the code

Xii. Going Further

As far as I can think of

  • Performance optimization, speed up response
  • Dockerfiles simplify deployment
  • Online code precompilation
  • Better testing