koa2+Vue构建SSR+CSR项目

本篇是使用koa2和vue构建SSR+CSR项目的总结

Posted by wang chong on February 12, 2019

实战型项目总结,总体目录结构如下。

koa-vue
├── assets                  静态资源
    ├── scripts                 js文件        
    ├── styles                  css文件
├── components              前端组件
├── controllers             控制器
├── config                  配置
├── docs                    文档
├── layout                  前端模板
├── logs                    日志文件
├── middlewares             中间件
├── models                  数据模型
├── node_modules            
├── routes                  路由
├── tests                   测试
├── utils                   工具类
├── views                   视图
└── .babelrc                babel
└── app.js                  启动文件
└── package.json            

开始

初始化项目

npm init -y

安装koa2

npm install koa –save

创建app.js

创建app.js并用koa搭建一个简单的服务

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx,next) => {
    ctx.body = 'Hello World';
});

app.listen(8000,()=>{
    console.log('Server is running...');
})

启动项目

使用supervisor启动,和nodemon一样。

安装supervisor

npm install supervisor -g

启动项目

supervisor app.js

路由管理

使用koa-simple-router包进行路由管理,因为这个包很简单。

分离路由逻辑。创建routes文件夹,并创建index.js文件

安装koa-simple-router

npm install koa-simple-router –save

在routes/index.js中书写

const router = require("koa-simple-router");

module.exports = (app)=>{
    app.use(router(_=>{
        _.get("/",async (ctx,next) => {
            ctx.body = '项目开始';   
        });
    }))
}

在app.js中,引入路由

const Koa = require('koa');
const app = new Koa();

-app.use(async (ctx,next) => {
-   ctx.body = 'Hello World';
-});
+ require('./routes')(app);
app.listen(8000,()=>{
    console.log('Server is running...');
})

控制器

分离逻辑,创建控制器目录。并创建SiteController文件

class SiteController {
    constructor(){};
    actionIndex(){
        return async(ctx)=>{
            ctx.body = 'Hello world';
        }
    }
}
module.exports = SiteController;

路由index引入控制器

const router = require("koa-simple-router");
const SiteController = require("../controllers/SiteController");
const siteController = new SiteController();

module.exports = (app)=>{
    app.use(router(_=>{
-        _.get("/",async (ctx,next) => {
-            ctx.body = '项目开始';   
-        });
+        _.get("/",siteController.actionIndex());
    }))
}

渲染视图

每一个node框架必不可少的一个。SSR渲染的根本。模板渲染使用swig,使用koa-swig这个包,koa2使用这个包依赖于co包,同时也要下载co包

安装包

npm install koa-swig co –save

在app.js中加载模板渲染

const Koa = require('koa');
+const render = require('koa-swig');
const co = require('co');
const app = new Koa();


+app.context.render = co.wrap(render({
+    root : __dirname + '/views',
+    autoescape : true,
+    cache : false,
+    ext : 'html',
+    writeBody :false
+}))

require('./routes')(app);
app.listen(8000,()=>{
    console.log('Server is running...');
})
视图模板

创建layout目录并创建layout.html文件,内容如下。 swig的内容请看这里

在views下面创建index.html.内容如下

修改SiteController内的内容

class SiteController {
    constructor(){};
    actionIndex(){
        return async(ctx)=>{
-            ctx.body = '项目开始';   
+            ctx.body = await ctx.render('index');
        }
    }
}
module.exports = SiteController;

静态资源

项目开发过程中静态资源是必不可少的。使用koa-static包进行静态资源的管理。

安装包

npm install koa-static –save

在app.js中使用koa-static

const Koa = require('koa');
const render = require('koa-swig');
+const static = require('koa-static');
const co = require('co');
const app = new Koa();

//静态资源
app.use(static(__dirname+'/assets'));
//模板渲染
app.context.render = co.wrap(render({
    root : __dirname + '/views',
    autoescape : true,
    cache : false,
    ext : 'html',
    writeBody :false
}))
//路由管理
require('./routes')(app);
app.listen(8000,()=>{
    console.log('Server is running...');
})

在项目中创建assets目录,并在其中创建scripts、styles目录。分别在其中创建index.js和index.css 内容如下:

body{
    backgroud-color:red;
}
console.log('hello world');

在views/index.html中引用这两个文件。

日志管理

日志在每一个项目里面都是比不可少的东西。log4js是一个不错的选择。

但是log4js还是有不好的地方:生产消费不及时,当用户来访问的时候,log4在记日志,让用户都走了,还在记日志。

安装log4js

npm install log4js –save

创建middlewars目录,并创建logHandler.js用于日志管理。

创建logs目录,用于存放日志

const log4js = require("log4js");
module.exports = ()=>{
    log4js.configure({
        appenders:{
            cheese:{
                type : 'file',
                filename : __dirname + '../logs/log.log',
            }
        },
        categories : {
            default : {
                appenders :['cheese'],
                level : 'error',
            }
        }
    
    })
    return log4js.getLogger('cheese');
}

app.js中引入日志管理。

+ const errorHandle = require("./middlewares/errorHandle");

容错处理

node的容错处理是一大难关,掌握了node的容错就掌握了大半江山。

创建middlewares目录,并创建errHandler.js用于书写错误处理。内容如下:

const errorHandle = {
    /**
     *用于Koa2项目容错机制
     *
     * @param {Object} app
     * @param {Object} logger
     */
    error(app,logger){
        //500容错
        app.use(async (ctx,next)=>{
            try{
                await next();
            }catch(error){
                ctx.body="500报错";
                //引入日志用于管理错误日志。
                logger.error(error);
            }
        })
        //404容错
        app.use(async (ctx,next) =>{
            //注意koa的执行机制,先next再回来判断。
            await next();
            if(404 != ctx.status){
                return ;
            }
            ctx.status = 200;
            //使用腾讯404公益页
            ctx.body=' <script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script>';
        })
    }
}
module.exports = errorHandle;

在app.js中使用

+const errorHandle = require("./middlewares/errorHandle");
//404 500容错机制
+errorHandle.error(app,logHandle);

配置文件

创建config目录,在其中创建index.js书写配置。

环境变量

一个项目中总是分为开发环境和生产环境,不同环境下的所使用的东西有些差异。使用cross-env来区分环境

安装

npm install cross-env –save

修改package中的scripts

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "cross-env NODE_ENV=development supervisor app.js",
    "prod": "cross-env NODE_ENV=production supervisor app.js",
  },

在config/index.js中书写以下代码

const { join } = require("path");
//使用lodash方法
const _ = require("lodash");
let config = {
    "viewDir": join(__dirname, "..", 'views'),  //视图模板路径
    'staticDir': join(__dirname, "..", "assets"),   //静态资源李静
    'logsDir' : join(__dirname ,'..',"logs/log.log"),   //日志文件路径
}
if (process.env.NODE_ENV == 'development') {
    let devConfig = {
        port : 8000,
    }
    config = _.extend(config,devConfig);
}
if (process.env.NODE_ENV == 'production') {
    let prodConfig = {
        port : 80,
    }
    config = _.extend(config,prodConfig);
}

module.exports = config;

基础的配置差不多就这样了。在响应的地方引入并且替换。

app.js中

const Koa = require("koa");
const static = require("koa-static");
const co = require("co");
const render = require("koa-swig");
const errorHandle = require("./middlewares/errorHandle");
const config = require("./config");
const logHandle = require("./middlewares/logHandle")();
const app = new Koa();

//模板渲染
app.context.render = co.wrap(render({
+    root : config.viewDir,
    autoescape : true,
    cache : false,
    ext : 'html',
    writeBody :false
}))
//静态资源
+app.use(static(config.staticDir));

//404 500容错机制
errorHandle.error(app,logHandle);
//路由管理
require("./routes/route")(app);

app.listen(config.port,()=>{
    console.log("Server is runnind...");
})

logHandler.js中

const config = require("../config");
const log4js = require("log4js");
module.exports = ()=>{
    log4js.configure({
        appenders:{
            cheese:{
                type : 'file',
+                filename : config.logsDir,
            }
        },
        categories : {
            default : {
                appenders :['cheese'],
                level : 'error',
            }
        }
    
    })
    return log4js.getLogger('cheese');
}

数据模型

如果当我们与后端交互的过程中,数据的是必须要做的。创建model目录,并创建Book.js。内容如下。

const SafeRequest = require("../utils/SafeRequest");

/**
 *@fileoverview 实现Book的数据模型
 *@author wang chong
 */

/**
 * Book类 获取后台关于图书相关的数据类
 * @class Book
 */
class Book{
    /**
     * @constructor
     * 
     */
    constructor(){}
    /**
     *获取后台全部图书的数据方法
     *
     * @example
     * return new Promise
     * getBooks()
     */
    getBooks(){
        const safeRequest = new SafeRequest("/index");
        return safeRequest.fetch({
            method : 'GET',
        })
    }
}

module.exports = Book;

上面代码中我们用到了SafeRequest,这个是用来做与后端接口的交互的。当然有个模型,与后端接口交互是必须的。由此之外我们还发现。每次都有注释,这个是必须的。使用工具生成文档才不会很乱。

SafeRequest类

SafeRequest类的作用是对后端的交互。

创建util目录并创建SafeRequest.js文件。内容如下:

const fetch = require("node-fetch");
const config = require('../config');
const logHandle = require("../middlewares/logHandle")();
const { URLSearchParams } = require("url");
/**
 *用于后端接口的工具类
 *
 * @class SafeRequest
 */
class SafeRequest{
    /**
     * @constructor
     * @param {string} url
     * @memberof SafeRequest
     */
    constructor(url){
        this.url = url;
        this.baseUrl = config.baseUrl;
    }
    /**
     * 与后端接口的请求方法
     * @param {Object} options 配置项
     * @returns {Promise} 
     * @memberof SafeRequest
     */
    fetch(options){
        let fetchRequest;
        if(options.method == 'GET'){
            fetchRequest = fetch(`${this.baseUrl}${this.url}${this._urlParams(options.params)}`)
        }else {
            fetchRequest = fetch(`${this.baseUrl}${this.url}`,{
                method : options.method,
                body: this._bodyParams(options.params),
                headers: { 'Content-Type': 'application/x-www-form-urlencoded'  },
            })
        }
        let result = {
            code : 0,
            messgae : '',
            data : [],
        }
        return new Promise((resolve,reject) =>{
            fetchRequest.then(res=>res.json())
                        .then(json=>{
                            result.code = 1,
                            result.message = 'ok',
                            result.data = json;
                            resolve(result);
                        })
                        //对接口的容错
                        .catch(error=>{
                            logHandle.error(error);
                            result.message = '与后端接口异常',
                            reject(result);
                        })
        //对自己的容错
        }).catch(error=>error);
    }
    /**
     * 工具类的方法,用于将对象转化成字符串连接在url后面
     * @param {Object} [params={}]
     * @returns {String} 
     * @memberof SafeRequest
     */
    _urlParams(params = {}){
        let paramsStr = "";
        for(let key in params){
            paramsStr += `&${key}=${params[key]}`;
        }
        return paramsStr;
    }
    /**
     * 工具类的方法,用于将对象转化成URLSearchParams对象,用于表单提交
     * @param {*} [params={}]
     * @returns {URLSearchParams}
     * @memberof SafeRequest
     */
    _bodyParams(params = {}){
        const param = new URLSearchParams();
        for(let key in params) {
            param.append(key,params[key]);
        }
        return param;
    }
}

module.exports = SafeRequest;

上面是我自己简单的封装的fetch方法。使用的是node-fetch包。同时包括对接口的容错。

数据模型文档

上面提到了把数据模型生成文档。这是很必要的,当项目越来越大的时候,有时候自己写的都分不清楚。生产文档有助于阅读。同时有助于团队开发。

使用jsdoc包进行项目文档整理。

npm install jsdoc –save-dev

修改package.json中的scripts

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "cross-env NODE_ENV=development supervisor app.js",
    "prod": "cross-env NODE_ENV=production supervisor app.js",
+    "docs": "jsdoc ./models/*.js -d ./docs/modeldocs",
  },

上面把文档生成在docs目录下。创建docs目录

启动生成

npm run docs

Vue

项目基本构建完毕,看了下标题。‘koa2+Vue构建SSR+CSR项目‘。一直都是koa2构建好像没有vue什么事。这时候事来了。

在项目中,SSR直出是比不可少的一部分。

问题
  1. 那我们的页面上的点击事件,增删改查怎么办呢?

    Vue来做。

  2. 那又有问题了。vue是构建spa的框架,怎么能和koa一块使用呢?

    在大型构建项目中,考虑整体项目的架构,把Vue当jQuery那样的工具来使用。即可以做到SSR,又可以做到CSR。

引入vue

在views/index.html中使用script标签的形式引入vue,并添加vue官网的一小段代码。

在assets/scripts/index.js中添加以下代码

var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})

启动我们预期的效果是出现Hello vue,但是世事难料,并没有出现。

原因是Vue的 数据模板和swig数据模板是相同的,引擎不能区分这到底是谁的。所以出现了分歧。导致失败。

怎么办呢?修改模板,Vue这个庞大的系统是可以的改它的,所以我们只能改swig。swig中有一个属性就是专门改数据模板的。

app.context.render = co.wrap(render({
    root:config.viewDir,
    autoescape: true,
    cache: false,
    ext: 'html',
    writeBody: false,
    varControls : ["[[","]]"],  //修改swig模板  避免与Vue相同
}));

修改完成之后重新测试下。

成功。

浏览器兼容ES6的问题

自从ES6出现之后,V8引擎是对ES6的语法有优化的。但是支持度不是很好,传统的方法就是开发时候使用ES6,上线使用babel打包成ES5。这样就浪费了V8对ES6的优化。灵活的使用ES6能提高代码的效率。请看我的浏览器支持ES6的最优解决方案

测试

最后的tests目录肯定是有用处的。目前而言由于项目的庞大,为了保证项目在线上的种种问题测试是必不可少的。有关测试请看JavaScript集成化测试

总结

有了这些,就有了一个完整项目的开端,接下来可以随心所欲的构建自己的网站。