EggVueSsr实现前后端分离、服务器和客户端同构渲染

Egg + Vue + Ssr

下一代web开发框架

环境版本 && 模式

  • Egg 版本: ^2.x.x; 模式: MVC
  • Node 版本: ^8.x.x+
  • Npm 版本: ^5.x.x+
  • Webpack 版本: ^4.x.x
  • Vue 版本: ^2.5.0 模式:MVVM
  • egg-view-vue-ssr 版本: ^3.x.x

运行命令

  • 安装cli(非必需)
npm install easywebpack-cli -g
  • 安装依赖
npm install
  • 本地开发
npm run dev
  • 发布模式
npm run build 
  • 启动应用
npm start 

项目结构和基本规范


    ├── app
    │   ├── controller               //控制器(模式:C层)
    │   │   ├── test
    │   │       └── test.js
    │   ├── models                   //数据模型(模式:M层)
    │   │    ├── api                 //接口api
    │   │    ├── mocks               //数据处理
    │   │        ├── app
    │   │        └── home
    │   ├── extend
    │   ├── lib
    │   ├── middleware
    │   ├── mocks
    │   ├── proxy
    │   ├── router.js                 //服务器端路由
    │   ├── view                             //视图(模式:V层,工具生成)
    │   │   ├── about                 // 服务器编译的jsbundle文件
    │   │   │   └── about.js
    │   │   ├── home
    │   │   │     └── home.js         // 服务器编译的jsbundle文件
    │   │   └── layout                // 用于根据指定的layout生成对应的html页面, 用于服务器渲染失败时,采用客户端渲染
    │   │       └── layout.html
    │   └── web                       //视图(前端工程目录开发-->生成模式:V层)
    │       ├── asset                 // 存放公共js,css资源
    │       ├── framework             // 前端公共库和第三方库
    │       │   ├── fastclick
    │       │   │   └── fastclick.js
    │       │   ├── sdk
    │       │   │   ├── sdk.js
    │       │   ├── storage
    │       │   │   └── storage.js
    │       │   └── vue               // 与vue相关的公开代码
    │       │       ├── app.js        // 前后端调用入口, 默认引入componet/directive/filter
    │       │       ├── component.js  // 组件入口, 可以增加component目录,类似下面的directive
    │       │       ├── directive     // directive 目录,存放各种directive组件
    │       │       ├── directive.js  // directive引用入口
    │       │       └── filter.js     // filter引用入口
    │       ├── page                  // 前端页面和webpack构建目录, 也就是webpack打包配置entryDir
    │       │   ├── home              // 每个页面遵循目录名, js文件名, scss文件名, vue文件名相同
    │       │   │   ├── home.scss
    │       │   │   ├── home.vue
    │       │   │   ├── images        // 页面自有图片,公共图片和css放到asset下面
    │       │   │   │   └── icon_more.png
    │       │   │   └── w-week        // 页面自有组件,公共组件放到widget下面
    │       │   │       ├── w-week.scss
    │       │   │       └── w-week.vue
    │       │   └── test             // 每个页面遵循目录名, js文件名, scss文件名, vue文件名相同
    │       │       └── test.vue
    │       ├── store                // 引入vuex 的基本规范, 可以分模块
    │       │   ├── app
    │       │   │   ├── actions.js
    │       │   │   ├── getters.js
    │       │   │   ├── index.js
    │       │   │   ├── mutation-type.js
    │       │   │   └── mutations.js
    │       │   └── store.js
    │       └── component           // 公共业务组件, 比如loading, toast等, 遵循目录名, js文件名, scss文件名, vue文件名相同
    │           ├── loading
    │           │   ├── loading.scss
    │           │   └── loading.vue
    │           ├── test
    │           │   ├── test.vue
    │           │   └── test.scss
    │           └── toast
    │               ├── toast.scss
    │               └── toast.vue
    ├── build                       //  webpack 自定义配置入口, 会与默认配置进行合并(看似这么多,其实这里只是占个位说明一下)
    │   ├── base
    │   │   └── index.js            // 公共配置        
    │   ├──  client                 // 客户端webpack编译配置
    │   │   ├── dev.js
    │   │   ├── prod.js
    │   │   └── index.js
    │   ├──  server                 // 服务端webpack编译配置
    │   │    ├── dev.js
    │   │    ├── prod.js
    │   │    └── index.js
    │   └── index.js
    ├── config
    │   ├── config.default.js
    │   ├── config.local.js
    │   ├── config.prod.js
    │   ├── config.test.js
    │   └── plugin.js
    ├── doc
    ├── index.js
    ├── public                      // webpack编译目录结构, render文件查找目录
    │   ├── manifest.json           // 资源依赖表
    │   ├── static
    │   │   ├── css
    │   │   │   ├── home
    │   │   │   │   ├── home.07012d33.css
    │   │   │   └── test
    │   │   │       ├── test.4bbb32ce.css
    │   │   ├── img
    │   │   │   ├── change_top.4735c57.png
    │   │   │   └── intro.0e66266.png
    │   ├── test
    │   │   └── test.js
    │   └── vendor.js               // 生成的公共打包库

nodejs服务器层处理

配置路由router
/*文件目录:project/app/router.js*/
module.exports = app => {
  app.get('/', app.controller.home.home.index);
  app.get('/app/api/article/list', app.controller.app.app.list);
  app.get('/app/api/article/:id', app.controller.app.app.detail);
  app.get('/app(/.+)?', app.controller.app.app.index);
};
控制器
/*文件目录:project/app/controller/home/home.js*/
const Controller = require('egg').Controller;
const Mocks = require('../../models/mocks/home/init');

class HomeController extends Controller {
  mocks(){
    const { ctx } = this;
    return  new Mocks({ctx: ctx});
  }
  async index() {//页面
    const { ctx  } = this;
    const mocks = this.mocks();
    await ctx.render('index/index.js', {
      title: 'egg vue ssr',
      list: await mocks.index() //发接口获取的数据
    });
  }
  async pager() {//接口
    const { ctx } = this;
    const mocks = this.mocks();
    const pageIndex = ctx.query.pageIndex;
    const pageSize = ctx.query.pageSize;
    ctx.body = await mocks.index();//发接口获取的数据
  }
};

module.exports = HomeController;
控制器:ctx对象常用解说
  • 获取页面参数(params)
//获取(46),比如:http://localhost:7001/app/detail/46
const id = this.ctx.params.id;
  • 获取页面参数(query)
//获取(20),比如:http://localhost:7001/?page=20
const page = this.ctx.query.page;
  • 获取页面协议(protocol)
//获取(20),比如:http://localhost:7001/?page=20
const protocol = this.ctx.protocol;
  • 渲染页面
await ctx.render('app/app.js', {});
  • 渲染接口数据
this.ctx.body = {title: '接口'};

了解更多

数据模型: API (必须传页面protocol值,否则使用config配置)
/* 文件目录:project/app/models/api/api.js */
const axios = require('axios');
const Config = require('../../config/config');

class Api {
  constructor(opts) {
    console.log(88777);
  }
  fetch(_opt) {
    var param = '',
      opt = Object.assign({
        baseHost: Config.apiHost,
        protocol: Config.apiProtocol,
        urlMap: '',
        url: '',
        method: 'GET',
        type: 'json',
        cookies: true, //Boolean
        timeout: 10000,
        param: null, //{id: 123}
        paramType: 0 // 0表示参数以字符串形式提交比如“wen=12&xx=333”; 1表示参数以对象形式提交比如“{wen:12, xx:33}”
      }, _opt);

    //考虑redis缓存处理

    if (opt.urlMap !== '') {
      opt.url = `${opt.baseHost}${opt.urlMap}`;
    }

    if (opt.protocol === 'https') {
      opt.url = (opt.url).replace(/^http:(\/\/[\w])/, 'https:$1');
    }
    console.log('opt.url', opt.url);
    if (opt.param !== null) {
      for (var key in opt.param) {
        if (typeof opt.param[key] !== 'function') {
          param += '&'+key+'='+ encodeURIComponent(opt.param[key]);
        }
      }
      param = param.substring(1);
    }

    return axios(opt.url, {
      method: opt.method,
      timeout: opt.timeout,
      data: (opt.paramType === 0) ? param : opt.param,
      withCredentials: opt.cookies
    }).then(function(response) {
      return response.data;
    }).catch(function(ex) {
      return { error: true, url: ex };
    });
  }
}

//export default Api;
module.exports = new Api();
数据模型:mocks
/*目录文件:project/app/models/mocks/app/init.js*/
const { fetch } = require('../../api/api');

class Mocks  {
    constructor(suppor) {
        this.ctx = suppor.ctx;
    }
    index(){
        let urlMap = '/menus',
            protocol = this.ctx.protocol;
        return fetch({
            url: 'http://m.aipai.com/mobile/apps/apps.php?module=gameIndex&func=newAsset&sort=click&appId=11616&page=3&pageSize=12',
            //urlMap: urlMap,
            protocol: protocol,//ctx.protocol,
            method: 'get',
            type: "jsonp",
            //param: param
        }).then(ret=>{
            //访问超时or资源地址出错
            if(typeof ret.error !== 'undefined' && ret.error){
                //msg = '网络错误';
            }else{  }
            return ret;
        });
    }
};
module.exports = Mocks;

客户端

获取服务端数据:serverData (没有store和router)
//使用计算属性
computed: {
  title(){
    return this.serverData.title;
  },
  lists(){
    return this.serverData.list;
  }
},
code01.png
获取服务端数据:serverData 有store和router)
/*目录文件:project/app/web/page/app/views/index.vue*/
preFetch ({ state, dispatch, commit }) {//只在服务器端执行; preFetch比created执行快
  return Promise.all([
    dispatch('FETCH_ARTICLE_LIST_PRE')
  ])
},
beforeMount() {//只在客户端执行; created比beforeMount执行快
  let serverData = this.$store.state.serverData;
  if(serverData.articleList && serverData.articleList.length >0){
    this.$store.commit('SET_ARTICLE_LIST', serverData.articleList);
  }else{
    return Promise.all([
      this.$store.dispatch('FETCH_ARTICLE_LIST')
    ]);
  }
}
code02.png

页面配置

设置页面的:标题、关键词、描述;引入css、js;
  • pluginCss: 数组;头部引入css
  • pluginJs: 数组;头部引入js
  • pluginFooterJs: 数组;底部引入js

    
    

/*目录文件:project/app/web/page/app/app.vue*/
computed: {
    layoutData() {
      let state = this.$store.state;
      return {
        title: state.serverData.title+": 2018世界杯大数据报告",
        keywords: "keywords",
        description: "description",
        pluginCss: ['//www.xxx.com/static/index.min.css'],
        pluginJs: ["//www.xxx.com/static/libs/zepto.min.1.2.0.js"],
        pluginFooterJs: ["//www.xxx.com/static/libs/zepto.min.1.2.0.js"],
      };
    }
 },

注意事项

备注: 服务端与客户端数据通过渲染window.__INITIAL_STATE__来桥接的
  • created 服务器端、客户端执行
  • preFetch (有store&&router时才存在方法)服务端执行;必须设置router为顶级路由,比如:服务器端控制器必须带 url: ctx.url.replace(//app/, '')
  • beforeMount 客户端执行
  • 在服务器端执行nodejs(全局:global、process、console)
  • 在客户端执行javascript(全局:window)

相关资料

  • nodejs
  • npmjs
  • webpack
  • egg
  • egg-plugin
  • koajs
  • vue
  • vuex
  • vue router
  • vue-devtools
  • Egg+Vue解决方案开发流程
  • 基于webpack的前端工程解决方案和egg+vue服务端渲染项目实践
  • koa和egg项目webpack内存编译和热更新实现
  • easywebpack
  • boilerplate

你可能感兴趣的:(EggVueSsr实现前后端分离、服务器和客户端同构渲染)