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;
}
},
获取服务端数据: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')
]);
}
}
页面配置
设置页面的:标题、关键词、描述;引入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