Authored by zhangxiaoru

nerge feature/ssr

@@ -22,6 +22,7 @@ const session = require('express-session'); @@ -22,6 +22,7 @@ const session = require('express-session');
22 const memcached = require('connect-memcached'); 22 const memcached = require('connect-memcached');
23 const pkg = require('./package.json'); 23 const pkg = require('./package.json');
24 const devtools = require('./doraemon/middleware/devtools'); 24 const devtools = require('./doraemon/middleware/devtools');
  25 +const _ = require('lodash');
25 26
26 const uuid = require('uuid'); 27 const uuid = require('uuid');
27 const app = express(); 28 const app = express();
@@ -48,10 +49,11 @@ if (app.locals.devEnv) { @@ -48,10 +49,11 @@ if (app.locals.devEnv) {
48 app.use(global.yoho.hbs({ 49 app.use(global.yoho.hbs({
49 extname: '.hbs', 50 extname: '.hbs',
50 defaultLayout: 'layout', 51 defaultLayout: 'layout',
  52 +
51 layoutsDir: path.join(__dirname, 'doraemon/views'), 53 layoutsDir: path.join(__dirname, 'doraemon/views'),
52 partialsDir: path.join(__dirname, 'doraemon/views/partial'), 54 partialsDir: path.join(__dirname, 'doraemon/views/partial'),
53 views: path.join(__dirname, 'doraemon/views'), 55 views: path.join(__dirname, 'doraemon/views'),
54 - helpers: global.yoho.helpers 56 + helpers: _.assign(global.yoho.helpers, require('./utils/helpers'))
55 })); 57 }));
56 58
57 app.use(favicon(path.join(__dirname, '/public/favicon.ico'))); 59 app.use(favicon(path.join(__dirname, '/public/favicon.ico')));
@@ -5,19 +5,40 @@ @@ -5,19 +5,40 @@
5 */ 5 */
6 'use strict'; 6 'use strict';
7 const channelModel = require('../models/channel'); 7 const channelModel = require('../models/channel');
  8 +const channelMap = {
  9 + men: '9ee58aadd9559d07207fe4a98843eaac',
  10 + women: '3ad8826fc89fb0d023a4cd06a6991219',
  11 + lifestyle: 'aa8d34c85934c2ccc16e2babd3eb5e47'
  12 +};
  13 +const _ = require('lodash');
8 14
9 /** 15 /**
10 * 频道选择页 16 * 频道选择页
11 */ 17 */
12 module.exports = { 18 module.exports = {
13 - index(req, res) { 19 + index(req, res, next) {
14 let channel = req.path.split('/')[1] || req.yoho.channel; 20 let channel = req.path.split('/')[1] || req.yoho.channel;
15 21
16 - res.render('cindex', { 22 + channelModel.getResourcesData({
  23 + contentCode: channelMap[channel]
  24 + }).then(result => {
  25 + const resources = result.slice(0, 3);
  26 +
  27 + _.each(resources, (resource) => {
  28 + // 只拿第一个数据
  29 + if (_.isArray(resource.data)) {
  30 + resource.data = [resource.data[0]];
  31 + }
  32 + });
  33 +
  34 + res.locals.resources = resources;
  35 +
  36 + res.render('index', {
17 module: 'channel', 37 module: 'channel',
18 page: 'home', 38 page: 'home',
19 channel: channel 39 channel: channel
20 }); 40 });
  41 + }).catch(next);
21 }, 42 },
22 channel(req, res, next) { 43 channel(req, res, next) {
23 channelModel.getChannelData().then(result => { 44 channelModel.getChannelData().then(result => {
1 <div id="channel"> 1 <div id="channel">
2 - <channel></channel> 2 + <div id="ssr" class="resources" style="position: absolute; top: 2.25rem;">
  3 + {{#resources}}
  4 + {{#if focus}}
  5 + <div class="focus-floor" style="height: 9.1rem;">
  6 + {{#data}}
  7 + <a href="{{this.url}}" title="{{this.title}}">
  8 + <img src="{{image2 this.src w=750 h=365 q=60}}" width="100%" alt="">
  9 + </a>
  10 + {{/data}}
  11 + </div>
  12 + {{/if}}
  13 +
  14 + {{#if titleImage}}
  15 + <div class="title-image">
  16 + {{#data}}
  17 + <div class="floor-header">
  18 + {{title}}
  19 +
  20 + <a class="more" href="{{moreUrl}}">
  21 + {{#eq moreName '...'}}
  22 + <span class="icon icon-more"></span>
  23 + {{^}}
  24 + <!--<span>{{{moreName}}}</span>-->
  25 + {{/eq}}
  26 + </a>
  27 + </div>
  28 +
  29 + <a class="image" href="{{image.url}}">
  30 + <img src="{{image2 image.src w=750 h=365 q=60}}" width="100%" alt="">
  31 + </a>
  32 + </div>
  33 + {{/data}}
  34 +
  35 + {{/if}}
  36 +
  37 + {{/resources}}
  38 + </div>
  39 +
  40 + <channel>
  41 + </channel>
3 </div> 42 </div>
@@ -35,18 +35,32 @@ const saveRecentGoodInCookies = (oldSkns, res, addSkns) => { @@ -35,18 +35,32 @@ const saveRecentGoodInCookies = (oldSkns, res, addSkns) => {
35 * 商品详情 35 * 商品详情
36 */ 36 */
37 const component = { 37 const component = {
38 - index(req, res) { 38 + index(req, res, next) {
39 const pid = req.params[0], 39 const pid = req.params[0],
40 goodsId = req.params[1], 40 goodsId = req.params[1],
41 cnAlphabet = req.params[2]; 41 cnAlphabet = req.params[2];
42 42
43 - res.render('pdetail', { 43 + let params = {
  44 + product_id: _.toString(pid),
  45 + uid: req.user.uid || 0
  46 + };
  47 +
  48 + model.product(params).then(product => {
  49 + product = product.data || {};
  50 +
  51 + product.formatPrice = product.formatSalesPrice !== '0' ? product.formatSalesPrice : product.formatMarketPrice;
  52 + product.isDiscount = product.marketPrice > product.salesPrice;
  53 +
  54 + console.log(product);
  55 + res.render('detail', {
44 module: 'product', 56 module: 'product',
45 page: 'detail', 57 page: 'detail',
46 pid: pid, 58 pid: pid,
47 goodsId: goodsId, 59 goodsId: goodsId,
48 - cnAlphabet: cnAlphabet 60 + cnAlphabet: cnAlphabet,
  61 + product: product
49 }); 62 });
  63 + }).catch(next);
50 }, 64 },
51 product(req, res, next) { 65 product(req, res, next) {
52 const pid = req.params[0]; // , goodsId = req.params[1]; 66 const pid = req.params[0]; // , goodsId = req.params[1];
@@ -8,11 +8,17 @@ const searchModel = require('../models/search'); @@ -8,11 +8,17 @@ const searchModel = require('../models/search');
8 8
9 9
10 /* 搜索 页面 */ 10 /* 搜索 页面 */
11 -exports.index = (req, res) => { 11 +exports.index = (req, res, next) => {
  12 + const params = req.query;
  13 +
  14 + searchModel.products(params).then(result => {
12 res.render('product-list', { 15 res.render('product-list', {
  16 + navTitle: params.title || params.sort_name,
13 module: 'product', 17 module: 'product',
14 - page: 'list' 18 + page: 'list',
  19 + list: result && result.data ? result.data.productList : []
15 }); 20 });
  21 + }).catch(next);
16 }; 22 };
17 23
18 /* 查询 产品列表 method:GET */ 24 /* 查询 产品列表 method:GET */
1 <div id="app" class="product-page" data-pid="{{pid}}" data-goods-id="{{goodsId}}"> 1 <div id="app" class="product-page" data-pid="{{pid}}" data-goods-id="{{goodsId}}">
  2 + {{#product}}
  3 + <div class="ssr show-box first-box">
  4 + <div class="image-carousel">
  5 + <div class="swipe">
  6 + <div class="swipe-items-wrap">
  7 + {{#each goodsList}}
  8 + {{#if colorImage}}
  9 + <div class="swipe-item {{#if @first}}active{{/if}}"> <img width="100%" alt="" src="{{image2 colorImage w=750 h=1000 q=80}}"></div>
  10 + {{/if}}
  11 + {{/each}}
  12 + </div>
  13 + </div>
  14 + </div>
  15 + <div class="title-box">
  16 + <h1 class="line-clamp-2">{{productName}}</h1>
  17 + {{#if isDiscount}}
  18 + <i class="price strike-through">{{formatMarketPrice}}</i>
  19 + {{/if}}
  20 + <i class="price {{#if isDiscount}}highlight{{/if}}">{{formatPrice}}</i>
  21 + </div>
  22 + </div>
  23 + {{#brandInfo}}
  24 + <div class="ssr show-box brand">
  25 + <img src="{{image2 brandIco w=110 h=68 q=80}}">
  26 + <h2>{{brandName}}</h2>
  27 + <div class="brand-go">
  28 + <span>进入店铺</span>
  29 + <span class="icon icon-right"></span>
  30 + </div>
  31 + <a href="/product/shop/{{brandDomain}}"></a>
  32 + </div>
  33 + {{/brandInfo}}
  34 + {{/product}}
2 <app/> 35 <app/>
3 </div> 36 </div>
  1 +<div id="ssr" style="position: absolute; top: 0;">
  2 + <div class="blk-header-wrap" class="is-fixed">
  3 + <div class="blk-header">
  4 + <div class="blk-header-left">
  5 + <i class="icon icon-left go-back-btn"></i>
  6 + </div>
  7 + <div class="blk-header-right">
  8 + </div>
  9 + <div class="blk-header-main">
  10 + <span class="blk-header-title">{{navTitle}}</span>
  11 + </div>
  12 + </div>
  13 + <div class="blk-header-gap"></div>
  14 + </div>
  15 +
  16 + <ul class="order-navs clearfix">
  17 + <li class="order-item active">
  18 + <span class="order-name">默认</span>
  19 + </li>
  20 + <li class="order-item">
  21 + <span class="order-name">最新</span>
  22 + </li>
  23 +
  24 + <li class="order-item">
  25 + <span class="order-name">价格</span>
  26 + <span class="order-icon">
  27 + <i class="icon icon-sort-up"></i>
  28 + <i class="icon icon-sort-down"></i>
  29 + </span>
  30 + </li>
  31 + <li class="order-item">
  32 + <span class="order-name">折扣</span>
  33 + <span class="order-icon">
  34 + <i class="icon icon-sort-up"></i>
  35 + <i class="icon icon-sort-down"></i>
  36 + </span>
  37 + </li>
  38 + </ul>
  39 +
  40 + <div class="goods-box">
  41 + <ul class="cardlist card-large clearfix">
  42 + {{#list}}
  43 + <li class="card">
  44 + <div class="card-pic">
  45 + <a href="{{goodsUrl this}}">
  46 + <img src="{{resizeImage defaultImages 372 499}}" alt="{{productName}}">
  47 + </a>
  48 + </div>
  49 + <div class="card-bd">
  50 + <h2 class="card-label">
  51 + <a href="{{goodsUrl this}}" class="line-clamp-2">{{productName}}</a>
  52 + </h2>
  53 + {{#if marketPrice}}
  54 + <span class="good-price" class="old-price">
  55 + ¥ {{toFixed marketPrice}}
  56 + </span>
  57 + {{/if}}
  58 + <span class="good-price {{#if marketPrice}}sale-price{{/if}}">¥ {{toFixed salesPrice}}</span>
  59 + </div>
  60 + </li>
  61 + {{/list}}
  62 + </ul>
  63 + </div>
  64 +</div>
1 <div id="product-list"></div> 65 <div id="product-list"></div>
@@ -40,9 +40,9 @@ module.exports = { @@ -40,9 +40,9 @@ module.exports = {
40 useOneapm: false, 40 useOneapm: false,
41 useCache: false, 41 useCache: false,
42 memcache: { 42 memcache: {
43 - master: ['192.168.102.205:12111'],  
44 - slave: ['192.168.102.205:12111'],  
45 - session: ['192.168.102.205:12111'], 43 + master: ['127.0.0.1:11211'],
  44 + slave: ['127.0.0.1:11211'],
  45 + session: ['127.0.0.1:11211'],
46 timeout: 1000, 46 timeout: 1000,
47 retries: 0 47 retries: 0
48 }, 48 },
@@ -15,6 +15,17 @@ yoho.ready(() => { @@ -15,6 +15,17 @@ yoho.ready(() => {
15 el: '#app', 15 el: '#app',
16 components: { 16 components: {
17 app: app 17 app: app
  18 + },
  19 + created() {
  20 + setTimeout(() => {
  21 + let ssrs = document.querySelectorAll('.ssr') || [];
  22 +
  23 + ssrs.forEach(i => {
  24 + if (i) {
  25 + i.remove();
  26 + }
  27 + });
  28 + }, 500);
18 } 29 }
19 }); 30 });
20 }); 31 });
@@ -93,6 +93,10 @@ @@ -93,6 +93,10 @@
93 if (result.length) { 93 if (result.length) {
94 dataCache[param] = result; 94 dataCache[param] = result;
95 } 95 }
  96 +
  97 + setTimeout(()=> {
  98 + $('#ssr').remove();
  99 + }, 2000);
96 }).fail(() => { 100 }).fail(() => {
97 tip('网络错误'); 101 tip('网络错误');
98 }); 102 });
@@ -116,6 +116,12 @@ @@ -116,6 +116,12 @@
116 }) 116 })
117 .always(() => { 117 .always(() => {
118 self.inSearching = false; 118 self.inSearching = false;
  119 +
  120 + // 完成后删除服务端渲染的元素
  121 + setTimeout(()=> {
  122 +
  123 + $('#ssr').remove();
  124 + });
119 }); 125 });
120 }, 126 },
121 127
  1 +'use strict';
  2 +
  3 +const url = require('url');
  4 +const _ = require('lodash');
  5 +const config = require('../config/common');
  6 +const assetUrl = config.assetUrl;
  7 +
  8 +module.exports = {
  9 + imgSrc: function(imgSrc) {
  10 + return url.resolve(assetUrl, imgSrc);
  11 + },
  12 + image2: function(imageUrl, opts) {
  13 + if (imageUrl && _.isString(imageUrl)) {
  14 + let params = opts.hash;
  15 + let urls = imageUrl.split('?');
  16 + let query = urls[1] || '';
  17 + let uri = urls[0];
  18 +
  19 + if (uri.indexOf('http:') === 0) {
  20 + uri = uri.replace('http:', '');
  21 + }
  22 +
  23 + if (query) {
  24 + query = query.replace(/{width}/g, params.w).replace(/{height}/g, params.h).replace(/{mode}/g, (params.mode || 2));
  25 +
  26 + if (query.indexOf('imageView2') === 0) {
  27 + if (params.q && query.indexOf('/q/') > 0) {
  28 + query = query.replace(/\/q\/\d+/g, '/q/' + params.q);
  29 + } else if (params.q) {
  30 + query += '/q/' + params.q;
  31 + }
  32 + } else if (query.indexOf('imageMogr2') === 0) {
  33 + if (params.q && query.indexOf('/quality/') > 0) {
  34 + query = query.replace(/\/quality\/\d+/g, '/quality/' + params.q);
  35 + } else if (params.q) {
  36 + query += '/quality/' + params.q;
  37 + }
  38 + } else if (query.indexOf('imageView/') === 0) {
  39 + if (params.q && query.indexOf('/q/') > 0) {
  40 + query = query.replace(/\/q\/\d+/g, '/q/' + params.q);
  41 + } else if (params.q) {
  42 + query += '/q/' + params.q;
  43 + }
  44 +
  45 + if (params.mode) {
  46 + query = query.replace(/imageView\/\d{1}\//, 'imageView/' + params.mode + '/');
  47 + }
  48 + }
  49 + } else {
  50 + query = 'imageView2/2/interlace/1/q/' + (params.q || 75);
  51 + }
  52 + return uri + '?' + query;
  53 + } else {
  54 + return '';
  55 + }
  56 + },
  57 + ifor: function() {
  58 + var args = Array.prototype.slice.call(arguments);
  59 + var opt = args[args.length - 1];
  60 + var isTrue = false;
  61 +
  62 + for (var i = 0; i < args.length - 1; i++) {
  63 + if (args[i]) {
  64 + isTrue = true;
  65 + break;
  66 + }
  67 + }
  68 +
  69 + if (isTrue) {
  70 + return opt.fn(this);
  71 + } else {
  72 + return opt.inverse(this);
  73 + }
  74 + },
  75 +
  76 + /**
  77 + * 小于某zhi
  78 + *
  79 + * @param variable
  80 + * @param number
  81 + */
  82 + within: function(variable, number, opt) {
  83 + if (variable < number) {
  84 + return opt.fn(this);
  85 + } else {
  86 + return opt.inverse(this);
  87 + }
  88 + },
  89 + eq: function(a, b, options) {
  90 + if (arguments.length === 2) {
  91 + options = b;
  92 + b = options.hash.compare;
  93 + }
  94 + if (a === b) {
  95 + return options.fn(this);
  96 + }
  97 + return options.inverse(this);
  98 + },
  99 + goodsUrl: (product, kind) => {
  100 + let productId, goodsId, cnAlphabet;
  101 +
  102 + switch (kind) {
  103 + case 'collection':
  104 + productId = product.productId;
  105 + goodsId = product.goodsId;
  106 + cnAlphabet = product.cnAlphabet;
  107 + break;
  108 + default:
  109 + productId = product.productId;
  110 + goodsId = product.goodsList[0].goodsId;
  111 + cnAlphabet = product.cnAlphabet;
  112 + }
  113 +
  114 + return `/product/pro_${productId}_${goodsId}/${cnAlphabet}.html`;
  115 + },
  116 + toFixed: (value, num) => {
  117 + if (typeof value === 'undefined') {
  118 + return;
  119 + }
  120 + return Number(value).toFixed(num || 2);
  121 + },
  122 + resizeImage: (src, width, height, mode) => {
  123 + return src ? src.replace(/(\{width}|\{height}|\{mode})/g, function($0) {
  124 + const dict = {
  125 + '{width}': width || 300,
  126 + '{height}': height || 300,
  127 + '{mode}': mode || 2
  128 + };
  129 +
  130 + return dict[$0];
  131 + }).replace(/https?:/, '') + '/interlace/1' : '';
  132 + }
  133 +};