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
<layout :layoutData="layoutData">
    <router-view></router-view>
    <!-- <transition name="fade" mode="out-in">
      
    </transition> -->
</layout>
/*目录文件: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)

相关资料

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,247评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,520评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,362评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,805评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,541评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,896评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,887评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,062评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,608评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,356评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,555评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,077评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,769评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,175评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,489评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,289评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,516评论 2 379

推荐阅读更多精彩内容