最近公司要求开发web版的app,因为app是偏向内容方面,并且带了一个聊天模块,因此通常的多页开发不是很适合,并且主要是手机浏览,对加载速度或者用户体验来讲都比较苛刻。调研了不少框架和模式,最后本身东拼西凑搞出来了这么一个玩意。javascript
毫无疑问使用node,使用typescript能够有效的在编码同时查错,强类型语言写服务端毫无压力。css
#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内容html
安卓微信浏览器 vue-resource 设置了headers的apikey,但请求的时候没有带上,致使获取不到数据。前端
IOS safari浏览器 渲染页面有问题,渲染20条数据,只显示10条左右,监听不到SUI无限滚动到底部的事件vue
不放源码都是瞎扯。html5