业务需求
最近公司要求开发web版的app,由于app是偏向内容方面,而且带了一个聊天模块,所以一般的多页开发不是很适合,而且主要是手机浏览,对加载速度或者用户体验来说都比较苛刻。调研了很多框架和模式,最后自己东拼西凑搞出来了这么一个玩意。
UI框架 :淘宝SUI
服务端
毫无疑问使用node,使用typescript可以有效的在编码同时查错,强类型语言写服务端毫无压力。
#app.ts 只贴重要代码
var webpack = require('webpack')
var webpackDevMiddleware = require('webpack-dev-middleware')
var WebpackConfig = require('./webpack.config')
import * as index from "./server/routes/index";
import * as cookbook from "./server/routes/cookbook";
import * as cookbookDetail from './server/routes/cookbookDetail'
var app = express();
//启动服务的时候 打包并监听客户端用到的文件,webpackDevMiddleware是开发模式,他会打包js在内存里面,你改了文件,它也会重新打包
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
publicPath: '/__build__/',
stats: {
colors: true
}
}));
//一般的配置项
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.set('view options', { layout: false });
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use(express.static(__dirname + '/public'));
var env = process.env.NODE_ENV || 'development';
if (env === 'development') {
app.use(errorHandler());
}
//路由配置
app.get('/cookbook', index.index);
app.get('/cookbook/:id', cookbook.index);
app.get('/cookbookDetail/:id', cookbookDetail.index);
app.listen(3000, function(){
console.log("Demo Express server listening on port %d in %s mode", 3000, app.settings.env);
});
export var App = app;
服务端渲染页面
#index.ts
import express = require("express")
import vueServer = require("vue-server") //服务端渲染vue的插件
import request = require('request'); //第3方http请求的插件
import queryString = require('querystring'); //转换get参数的插件
var Vue = new vueServer.renderer(); //创建一个服务端的vue
export function index(req: express.Request, res: express.Response) {
let vm:vueServer,
b:Object,
options:Object;
options = {
method: 'GET',
//随便用了一个免费的API,是查询菜谱的
url: 'http://apis.baidu.com/tngou/cook/classify?'+queryString.stringify({
id : 0,
}),
headers: {
//百度API的开放接口凭证
'apikey': 'a369f43a6392605426433831e10765ec'
}
};
request(options,function(err,resp,body){
if (!err && resp.statusCode == 200) {
b = JSON.parse(body);
vm = new Vue({
replace : false,
template : `
<div>
<!-- 标题栏 -->
<header class="bar bar-nav">
<a class="icon icon-me pull-left open-panel"></a>
<h1 class="title">{{title}}</h1>
</header>
<!-- 这里是页面内容区 -->
<div class="content">
<div class="list-block">
<ul>
<li class="item-content" v-for="item in cookbookClasses">
<div class="item-media"><i class="icon icon-f7"></i></div>
<div class="item-inner">
<div class="item-title">{{item.title}}</div>
</div>
</li>
</ul>
</div>
</div>
</div>
`,
data : {
title : '菜谱首页',
cookbookClasses: b.tngou,
}
});
}
//等待html渲染完成,再返回给浏览器 vueServer.htmlReady是vue-server的自带事件
vm.$on('vueServer.htmlReady', function(html:string) {
//这里用的是ejs模板 可以把需要用到的数据设置成window下的全局变量,方便客户端的js访问。
res.render('layout',{
server_html:html,
server_data:`
window.cm_cookbookClasses = {
title : '菜谱首页',
cookbookClasses: ${JSON.stringify(b.tngou)}
}`
})
});
});
}
#layout.ejs 访问这个SPA的所有url返回的都是这个页面 <meta>标签都可以动态设置,只要传参数进来就可以
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,initial-scale=1.0,user-scalable=no">
<title>Vue Router Example</title>
<link rel="stylesheet" href="//g.alicdn.com/msui/sm/0.6.2/css/??sm.min.css,sm-extend.min.css">
<style type="text/css">
//注释掉的是vue-router切换的动画, 通过transition,transition-mode设置
/*.test-transition {*/
/*transition: all .5s ease;*/
/*}*/
/*.test-enter, .test-leave {*/
/*opacity: 0;*/
/*transform: translate3d(10px, 0, 0);*/
/*}*/
h2{
font-size: 1rem;
}
p{
font-size: .8rem;
}
img{
max-width: 100%;
}
</style>
<script>
//定义一些前端需要用到的全局属性,文章ID或用户信息什么的
//index.ts中传过来的是 window.cm_data = {name:"张三"}
//前端就能访问到了
<%-server_data%>
</script>
</head>
//这里的id是前端需要用到的一个标识
<body id="app">
<div class="page-group">
<div class="page page-current">
//router-view是客户端vue-router需要解析的dom
//server_html是根据访问url地址生成的html,是做SEO的重点,不加载下面的js也可以看到内容
<router-view transition="test" transition-mode="out-in"><%-server_html%></router-view>
</div>
</div>
//这里用到了淘宝团队的UI框架SUI,各种组件都有。它依赖zepto
<script type='text/javascript' src='//g.alicdn.com/sj/lib/zepto/zepto.min.js' charset='utf-8'></script>
<script type='text/javascript' src='//g.alicdn.com/msui/sm/0.6.2/js/??sm.min.js,sm-extend.min.js' charset='utf-8'></script>
//webpack打包好的js,主要是路由配置
<script src="/__build__/app.js"></script>
</body>
</html>
客户端
#app.js 这个是/__build__/app.js,可以用es6编写,webpack会转换的
import Vue from './vue.min' //客户端的vue.js
import VueRouter from './vue-router.min' //vue的路由插件,配合webpack可以很简单实现懒加载
import VueResource from './lib/vue-resource.min' //懒加载路由 只有访问这个路由才会加载js
import Index from 'bundle?lazy!./routes/index' //配合webpack的bundle-loader,轻松实现懒加载
import Cookbook from 'bundle?lazy!./routes/Cookbook'
import CookbookDetail from 'bundle?lazy!./routes/cookbookDetail'
var App = Vue.extend({})
Vue.use(VueResource)
Vue.use(VueRouter)
//百度API需要用到的参数
Vue.http.headers.common['apikey'] = 'a369f43a6392605426433831e10765ec';
var router = new VueRouter({
//这里要好好说一下,一定要设置html5模式,不然前后端URL不统一会发生问题
//比如访问 http://localhost:3000/ 服务端定义是访问index.ts这个路由文件
//如果不是html5模式的话,经过客户端js运行之后会变成http://localhost:3000/#!/
//在比如直接浏览器输入 http://localhost:3000/foo 服务端定义是访问.ts这个路由文件
//如果不是html5模式的话,经过客户端js运行之后会变成 http://localhost:3000/foo/#!/
//设置了html5模式后,加载完js后不会加上#!这2个类似锚点的字符,实现前后端路由统一如果用户刷新浏览器的话,服务端也能渲染出相应的页面。
history: true, //html5模式 去掉锚点
saveScrollPosition: true //记住页面的滚动位置 html5模式适用,实际使用下来没用
})
//定义路由,要和服务端路由路径定义的一样
router.map({
'/' : {
component: Index //前端路由定义,
},
'/cookbook/:id': {
component: Cookbook
},
'/cookbookDetail/:id': {
component: CookbookDetail
}
})
router.redirect({
'*': '/cookbook'
})
//启动APP
router.start(App, '#app')
客户端路由
#index.js 这里的模板和服务端的差不多,就增加了@click操作
'use strict';
import Vue from '../lib/vue.min'
let Index = Vue.extend({
//replace : false, //必须注释掉 不然动画失效
template : `
<div>
<!-- 标题栏 -->
<header class="bar bar-nav">
<a class="icon icon-me pull-left open-panel"></a>
<h1 class="title">{{title}}</h1>
</header>
<!-- 这里是页面内容区 -->
<div class="content">
<div class="list-block">
<ul>
<li class="item-content" v-for="item in cookbookClasses" @click="goCookbook(item.id)">
<div class="item-media"><i class="icon icon-f7"></i></div>
<div class="item-inner">
<div class="item-title">{{item.title}}</div>
</div>
</li>
</ul>
</div>
</div>
</div>
`,
data : ()=>{
return {
title : '菜谱首页',
cookbookClasses : []
}
},
methods: {
goCookbook(id){
//vue-router 路由跳转
this.$router.go('/cookbook/'+id);
}
},
//vue-router的属性,可以设置路由的生命周期,具体请查文档
route : {
//应该是在渲染DOM之前获取数据
data : function(transition) {
//如果是服务端渲染的,应该设置全局变量,那么客户端就不用异步请求数据了
if(window.cm_cookbookClasses){
this.$data = window.cm_cookbookClasses;
transition.next();
}else{
let qa_id = 0;
//使用vue-resource来获取数据
var resource = this.$resource('http://apis.baidu.com/tngou/cook/classify');
$.showPreloader(); //这个是显示SUI的加载遮罩层
resource.get({id: qa_id}).then((response)=>{
$.hidePreloader();
if(response.status == 200){
this.$data = {
title : '菜谱首页',
cookbookClasses : response.data.tngou
}
transition.next();
}else{
transition.abort();
}
});
}
},
canActivate: function(){
},
// 激活状态 把上一次记录的数据,获取出来,需要deactivate状态配合。
activate: function (transition) {
this.$data = window.cm_cookbookClasses;
transition.next()
},
// 禁用状态 记录这一次的数据,方便以后再进入激活状态可以不用访问网络请求数据
deactivate: function (transition) {
window.cm_cookbookClasses = this.$data;
transition.next()
}
}
})
export default Index
需要完善的地方
前后端统一模板,已经找到方法了把html分离出来,node端用fs.readFileSync方法获取,客户端用webpack的raw-loader获取html内容
安卓微信浏览器 vue-resource 设置了headers的apikey,但请求的时候没有带上,导致获取不到数据。
IOS safari浏览器 渲染页面有问题,渲染20条数据,只显示10条左右,监听不到SUI无限滚动到底部的事件
不放源码都是瞎扯。