Authored by 陈峰

commit

@@ -21,6 +21,10 @@ export function createApp(context) { @@ -21,6 +21,10 @@ export function createApp(context) {
21 const app = new Vue({ 21 const app = new Vue({
22 router, 22 router,
23 store, 23 store,
  24 + errorCaptured(e) {
  25 + console.log('errorCaptured', e);
  26 + return false;
  27 + },
24 render: h => h(App) 28 render: h => h(App)
25 }); 29 });
26 30
@@ -2,7 +2,6 @@ const api = global.yoho.API; @@ -2,7 +2,6 @@ const api = global.yoho.API;
2 const service = global.yoho.ServiceAPI; 2 const service = global.yoho.ServiceAPI;
3 const checkParams = require('../../utils/check-params'); 3 const checkParams = require('../../utils/check-params');
4 const apiMaps = require('../../config/api-map'); 4 const apiMaps = require('../../config/api-map');
5 -const _ = require('lodash');  
6 5
7 const NOT_FOUND_API_MAP = { 6 const NOT_FOUND_API_MAP = {
8 code: 400, 7 code: 400,
@@ -13,30 +12,24 @@ const checkApiMap = url => { @@ -13,30 +12,24 @@ const checkApiMap = url => {
13 }; 12 };
14 const request = async({url, method, reqParams, context}) => { 13 const request = async({url, method, reqParams, context}) => {
15 const apiInfo = checkApiMap(url); 14 const apiInfo = checkApiMap(url);
  15 + const {env, user} = context;
16 16
17 if (!apiInfo) { 17 if (!apiInfo) {
18 return Promise.reject(NOT_FOUND_API_MAP); 18 return Promise.reject(NOT_FOUND_API_MAP);
19 } 19 }
20 - try {  
21 if (!apiInfo.service) { 20 if (!apiInfo.service) {
22 Object.assign(reqParams, { 21 Object.assign(reqParams, {
23 - uid: { 22 + uid: (user && user.uid) ? {
24 toString: () => { 23 toString: () => {
25 - return context.user.uid || 0;  
26 - },  
27 - sessionKey: context.user.sessionKey,  
28 - appSessionType: context.user.appSessionType 24 + return user.uid;
29 }, 25 },
  26 + sessionKey: user.sessionKey,
  27 + appSessionType: user.appSessionType
  28 + } : 1,
30 method: apiInfo.api, 29 method: apiInfo.api,
31 - sessionKey: context.user.sessionKey,  
32 - appVersion: context.user.appVersion  
33 }); 30 });
34 } 31 }
35 - if (_.has(apiInfo, 'params.uid') &&  
36 - apiInfo.params.uid.require !== false &&  
37 - reqParams.uid === 0) { // 如果接口uid是必须的但是有没有传入uid则直接返回空对象  
38 - return Promise.resolve({});  
39 - } 32 +
40 const params = checkParams.getParams(reqParams, apiInfo); 33 const params = checkParams.getParams(reqParams, apiInfo);
41 const cache = method.toLowerCase() !== 'get' ? false : apiInfo.cache; 34 const cache = method.toLowerCase() !== 'get' ? false : apiInfo.cache;
42 35
@@ -45,8 +38,9 @@ const request = async({url, method, reqParams, context}) => { @@ -45,8 +38,9 @@ const request = async({url, method, reqParams, context}) => {
45 cache: cache, 38 cache: cache,
46 code: 200, 39 code: 200,
47 headers: { 40 headers: {
48 - 'X-YOHO-IP': context.env.clientIp,  
49 - 'X-Forwarded-For': context.env.clientIp 41 + 'X-YOHO-IP': env.clientIp,
  42 + 'X-Forwarded-For': env.clientIp,
  43 + 'User-Agent': 'yoho/nodejs'
50 } 44 }
51 }); 45 });
52 } else { 46 } else {
@@ -54,17 +48,12 @@ const request = async({url, method, reqParams, context}) => { @@ -54,17 +48,12 @@ const request = async({url, method, reqParams, context}) => {
54 code: 200, 48 code: 200,
55 cache: cache, 49 cache: cache,
56 headers: { 50 headers: {
57 - 'X-YOHO-IP': context.env.clientIp,  
58 - 'X-Forwarded-For': context.env.clientIp 51 + 'X-YOHO-IP': env.clientIp,
  52 + 'X-Forwarded-For': env.clientIp,
  53 + 'User-Agent': 'yoho/nodejs'
59 } 54 }
60 }); 55 });
61 } 56 }
62 - } catch (e) {  
63 - return Promise.reject({  
64 - code: 400,  
65 - message: `create api:${e}`  
66 - });  
67 - }  
68 }; 57 };
69 58
70 export const createApi = context => { 59 export const createApi = context => {
@@ -52,4 +52,7 @@ router.onReady(() => { @@ -52,4 +52,7 @@ router.onReady(() => {
52 app.$mount('#app'); 52 app.$mount('#app');
53 }); 53 });
54 54
  55 +router.onError(e => {
  56 + router.push({name: 'error.500'});
  57 +});
55 58
1 import {createApp} from './app'; 1 import {createApp} from './app';
2 -import _ from 'lodash/core'; 2 +import {get} from 'lodash';
  3 +
3 import { 4 import {
4 SET_ENV, 5 SET_ENV,
5 - INIT_ROUTE_CHANGE  
6 } from 'store/yoho/types'; 6 } from 'store/yoho/types';
  7 +const sender = global.yoho.apmSender;
  8 +const logger = global.yoho.logger;
  9 +
  10 +const catchError = (err, context) => {
  11 + logger.error(`[catchError], ${err}`);
  12 + setImmediate(() => {
  13 + try {
  14 + sender.addMessage({
  15 + measurement: 'error-report',
  16 + tags: {
  17 + app: 'yoho-app', // 应用名称
  18 + hostname: context.hostname,
  19 + type: 'server',
  20 + route: context.route, // 请求路由
  21 + uid: get(context, 'user.uid', 0),
  22 + udid: context.udid,
  23 + code: err.code || 500,
  24 + path: context.path,
  25 + url: encodeURIComponent(context.url),
  26 + ip: context.env.clientIp
  27 + },
  28 + fields: {
  29 + message: err.message,
  30 + stack: err.stack,
  31 + useragent: context.ua
  32 + }
  33 + });
  34 + } catch (error) {
  35 + logger.error(error);
  36 + }
  37 + });
  38 +};
7 39
8 export default context => { 40 export default context => {
9 return new Promise((resolve, reject) => { 41 return new Promise((resolve, reject) => {
10 const {app, router, store} = createApp(context); 42 const {app, router, store} = createApp(context);
11 - const {url} = context; 43 + const {url, env} = context;
12 44
13 - const route = router.resolve(url).route;  
14 -  
15 - // if (url !== route.fullPath) {  
16 - // return reject({code: 500, message: 'url not matched', url: route.fullPath});  
17 - // }  
18 - store.commit(SET_ENV, context.env); 45 + store.commit(SET_ENV, env);
19 router.push(url); 46 router.push(url);
20 router.onReady(() => { 47 router.onReady(() => {
21 - try {  
22 const matched = router.getMatchedComponents(); 48 const matched = router.getMatchedComponents();
23 49
24 - if (!matched.length) {  
25 - reject({code: 404}); 50 + if (matched.some(m => !m)) {
  51 + catchError(new Error('导航组件为空'), context);
  52 + router.push({name: 'error.500'});
  53 + return resolve(app);
26 } 54 }
27 - const routes = [];  
28 - const rootRoute = _.find(router.options.routes, r => r.meta && r.meta.root);  
29 -  
30 - if (rootRoute) {  
31 - routes.push({  
32 - name: rootRoute.name,  
33 - fullPath: rootRoute.path  
34 - });  
35 - }  
36 -  
37 - if (route.name !== 'channel.home') {  
38 - routes.push({  
39 - name: route.name,  
40 - fullPath: route.fullPath  
41 - }); 55 + if (!matched.length) {
  56 + return reject({code: 404, message: ''});
42 } 57 }
43 58
44 - store.commit(INIT_ROUTE_CHANGE, {routes});  
45 Promise.all(matched.map(({asyncData}) => 59 Promise.all(matched.map(({asyncData}) =>
46 asyncData && asyncData({store, router: router.currentRoute}))) 60 asyncData && asyncData({store, router: router.currentRoute})))
47 .then(() => { 61 .then(() => {
48 context.state = store.state; 62 context.state = store.state;
49 - resolve(app);  
50 - }).catch((e) => {  
51 - reject({  
52 - code: 500,  
53 - message: e.stack || e.toString() 63 + return resolve(app);
  64 + }).catch(e => {
  65 + catchError(e, context);
  66 + return resolve(app);
54 }); 67 });
55 }); 68 });
56 - } catch (e) {  
57 - reject({  
58 - code: 500,  
59 - message: e.stack || e.toString() 69 +
  70 + router.onError(e => {
  71 + catchError(e, context);
  72 + router.push({name: 'error.500'});
  73 + return resolve(app);
60 }); 74 });
61 - }  
62 - }, reject);  
63 }); 75 });
64 }; 76 };
  1 +<template>
  2 + <div class="err-404">
  3 + 404
  4 + </div>
  5 +</template>
  6 +
  7 +<script>
  8 +export default {
  9 + name: 'ErrorNotFound'
  10 +}
  11 +</script>
  12 +
  13 +<style>
  14 +
  15 +</style>
  1 +<template>
  2 + <div class="err-500">
  3 + 500
  4 + <NotFound></NotFound>
  5 + </div>
  6 +</template>
  7 +
  8 +<script>
  9 +export default {
  10 + name: 'Error',
  11 + created() {
  12 + console.log('400 created')
  13 + },
  14 + mounted() {
  15 + console.log('400 mounted')
  16 + },
  17 + components: {NotFound: () => import('./404')}
  18 +}
  19 +</script>
  20 +
  21 +<style>
  22 +
  23 +</style>
  1 +export default [{
  2 + path: '/error/404',
  3 + name: 'error.404',
  4 + component: () => import(/* webpackChunkName: "error" */ './404')
  5 +}, {
  6 + path: '/error/500',
  7 + name: 'error.500',
  8 + component: () => import(/* webpackChunkName: "error" */ './500')
  9 +}];
  1 +import ErrorPages from './error';
  2 +
  3 +export default [...ErrorPages];
1 import Markfav from './markfav'; 1 import Markfav from './markfav';
2 import Single from './single'; 2 import Single from './single';
  3 +import Common from './common';
3 4
4 -export default [...Markfav, ...Single]; 5 +export default [...Markfav, ...Single, ...Common];
@@ -5,7 +5,7 @@ import routes from '../pages'; @@ -5,7 +5,7 @@ import routes from '../pages';
5 Vue.use(Router); 5 Vue.use(Router);
6 6
7 export function createRouter() { 7 export function createRouter() {
8 - return new Router({ 8 + const route = new Router({
9 mode: 'history', 9 mode: 'history',
10 routes, 10 routes,
11 scrollBehavior(to, from, savedPosition) { 11 scrollBehavior(to, from, savedPosition) {
@@ -16,4 +16,13 @@ export function createRouter() { @@ -16,4 +16,13 @@ export function createRouter() {
16 } 16 }
17 } 17 }
18 }); 18 });
  19 +
  20 + route.beforeEach((to, from, next) => {
  21 + if (!to.matched.length) {
  22 + return next({name: 'error.404'});
  23 + }
  24 + next();
  25 + });
  26 +
  27 + return route;
19 } 28 }
@@ -66,9 +66,9 @@ exports.createApp = async(app) => { @@ -66,9 +66,9 @@ exports.createApp = async(app) => {
66 app.use(userMiddleware); 66 app.use(userMiddleware);
67 app.use(ssrApiMiddleware); 67 app.use(ssrApiMiddleware);
68 68
69 - app.use(ssrRouteMiddleware); 69 + app.use(ssrRouteMiddleware.routers);
70 70
71 - app.all('*', errorMiddleware.notFound); // 404 71 + app.all('*', ssrRouteMiddleware.ssrRender); // 404
72 72
73 // YOHO 后置中间件 73 // YOHO 后置中间件
74 app.use(errorMiddleware.serverError); 74 app.use(errorMiddleware.serverError);
1 -/**  
2 - * 404 错误  
3 - * @return {[type]}  
4 - */  
5 const logger = global.yoho.logger; 1 const logger = global.yoho.logger;
6 2
7 -exports.notFound = (req, res) => {  
8 - res.status(404);  
9 -  
10 - if (req.xhr) {  
11 - return res.json({  
12 - code: 404,  
13 - message: '抱歉,页面不存在!'  
14 - });  
15 - }  
16 -  
17 - return res.render('error/404', {  
18 - module: 'common',  
19 - page: 'error',  
20 - title: '页面不存在 | BLK | 潮流购物逛不停',  
21 - noLocalCSS: true  
22 - });  
23 -};  
24 -  
25 /** 3 /**
26 * 服务器错误 4 * 服务器错误
27 * @return {[type]} 5 * @return {[type]}
28 */ 6 */
29 -exports.serverError = (err, req, res, next) => { 7 +exports.serverError = (err, req, res, next) => { // eslint-disable-line
30 logger.error(`error at path: ${req.url}`); 8 logger.error(`error at path: ${req.url}`);
31 - logger.error(err); 9 + logger.error(`${req.url},${err}`);
32 10
33 - if (!res.headersSent) {  
34 res.status(err.code || 500); 11 res.status(err.code || 500);
35 12
36 if (req.xhr) { 13 if (req.xhr) {
@@ -40,12 +17,5 @@ exports.serverError = (err, req, res, next) => { @@ -40,12 +17,5 @@ exports.serverError = (err, req, res, next) => {
40 }); 17 });
41 } 18 }
42 19
43 - return res.render('error/500', {  
44 - module: 'common',  
45 - page: 'error',  
46 - title: '服务器错误 | BLK | 潮流购物逛不停',  
47 - noLocalCSS: true  
48 - });  
49 - }  
50 - next(err); 20 + return res.send('服务器开小差了~');
51 }; 21 };
@@ -13,13 +13,13 @@ module.exports = async(req, res, next) => { @@ -13,13 +13,13 @@ module.exports = async(req, res, next) => {
13 13
14 if (!apiInfo.service) { 14 if (!apiInfo.service) {
15 baseParams = { 15 baseParams = {
16 - uid: { 16 + uid: (req.user && req.user.uid) ? {
17 toString: () => { 17 toString: () => {
18 return req.user.uid || 0; 18 return req.user.uid || 0;
19 }, 19 },
20 sessionKey: req.user.sessionKey, 20 sessionKey: req.user.sessionKey,
21 appSessionType: req.user.appSessionType 21 appSessionType: req.user.appSessionType
22 - }, 22 + } : 0,
23 method: apiInfo.api 23 method: apiInfo.api
24 }; 24 };
25 } 25 }
1 const fs = require('fs'); 1 const fs = require('fs');
2 const path = require('path'); 2 const path = require('path');
3 const url = require('url'); 3 const url = require('url');
4 -const sourceMap = require('source-map');  
5 const _ = require('lodash'); 4 const _ = require('lodash');
  5 +const os = require('os');
6 const md5 = require('yoho-md5'); 6 const md5 = require('yoho-md5');
7 const pkg = require('../../package.json'); 7 const pkg = require('../../package.json');
8 const routes = require('../../config/ssr-routes'); 8 const routes = require('../../config/ssr-routes');
9 const redis = require('../../utils/redis'); 9 const redis = require('../../utils/redis');
  10 +const routeEncode = require('../../utils/route-encode');
10 const {createBundleRenderer} = require('vue-server-renderer'); 11 const {createBundleRenderer} = require('vue-server-renderer');
11 const logger = global.yoho.logger; 12 const logger = global.yoho.logger;
12 const config = global.yoho.config; 13 const config = global.yoho.config;
13 14
14 -const REG_STACK = /at ([^:]+):(\d+):(\d+)/;  
15 -  
16 const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; 15 const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
17 16
18 let renderer; 17 let renderer;
@@ -41,11 +40,26 @@ const getContext = (req) => { @@ -41,11 +40,26 @@ const getContext = (req) => {
41 isiOS: req.yoho.isiOS, 40 isiOS: req.yoho.isiOS,
42 isAndroid: req.yoho.isAndroid, 41 isAndroid: req.yoho.isAndroid,
43 isYohoApp: req.yoho.isYohoApp, 42 isYohoApp: req.yoho.isYohoApp,
44 - clientIp: req.yoho.clientIp  
45 - } 43 + clientIp: req.yoho.clientIp,
  44 + },
  45 + ua: req.get('user-agent'),
  46 + hostname: os.hostname(),
  47 + route: `[${req.method}]${_.get(req, 'route.path', '')}`, // 请求路由
  48 + udid: _.get(req, 'cookies.udid', 'yoho'),
  49 + path: `[${req.method}]${routeEncode.getRouter(req)}`,
46 }; 50 };
47 }; 51 };
48 52
  53 +const handlerError = (err = {}, req, res, next) => {
  54 + if (err.code === 404) {
  55 + return res.redirect('/error/404');
  56 + } else if (err.code === 500) {
  57 + return res.redirect('/error/500');
  58 + }
  59 + console.log(err)
  60 + return next(err);
  61 +};
  62 +
49 const getCacheKey = (urlPath, cackeKey = '') => { 63 const getCacheKey = (urlPath, cackeKey = '') => {
50 const urlObj = url.parse(urlPath); 64 const urlObj = url.parse(urlPath);
51 65
@@ -54,37 +68,11 @@ const getCacheKey = (urlPath, cackeKey = '') => { @@ -54,37 +68,11 @@ const getCacheKey = (urlPath, cackeKey = '') => {
54 .replace('$params', urlObj.query)); 68 .replace('$params', urlObj.query));
55 }; 69 };
56 70
57 -const parseError = async({stack = ''}) => {  
58 - try {  
59 - const splits = stack.split('\n');  
60 - const lastError = splits.map(str => {  
61 - const match = str.match(REG_STACK);  
62 -  
63 - if (match) {  
64 - return {file: match[1], line: parseInt(match[2], 10), column: parseInt(match[3], 10)};  
65 - }  
66 - return false;  
67 - }).find(match => match);  
68 -  
69 - if (lastError && lastError.file) {  
70 - const consumer = await new sourceMap.SourceMapConsumer(serverBundle.maps[lastError.file]);  
71 -  
72 - const origin = consumer.originalPositionFor({  
73 - line: lastError.line,  
74 - column: 342  
75 - });  
76 -  
77 - console.log(origin);  
78 - }  
79 - } catch (error) {  
80 - logger.error(error);  
81 - }  
82 -};  
83 -  
84 const render = (route) => { 71 const render = (route) => {
85 return async(req, res, next) => { 72 return async(req, res, next) => {
  73 + try {
86 res.setHeader('X-YOHO-Version', pkg.version); 74 res.setHeader('X-YOHO-Version', pkg.version);
87 - const ck = getCacheKey(req.url, route.cackeKey); 75 + const ck = route.cackeKey ? getCacheKey(req.url, route.cackeKey) : void 0;
88 76
89 if (config.useCache && route.cache && ck) { 77 if (config.useCache && route.cache && ck) {
90 const html = await redis.getAsync(ck); 78 const html = await redis.getAsync(ck);
@@ -100,20 +88,23 @@ const render = (route) => { @@ -100,20 +88,23 @@ const render = (route) => {
100 88
101 renderer.renderToString(context, (err, html) => { 89 renderer.renderToString(context, (err, html) => {
102 if (err) { 90 if (err) {
103 - parseError(err);  
104 - return next(err.message); 91 + return handlerError(err, req, res, next);
105 } 92 }
106 if (config.useCache && route.cache && ck) { 93 if (config.useCache && route.cache && ck) {
107 redis.setex(ck, route.cacheTime || 60, html); 94 redis.setex(ck, route.cacheTime || 60, html);
108 } 95 }
109 return res.send(html); 96 return res.send(html);
110 }); 97 });
  98 + } catch (error) {
  99 + return next(error);
  100 + }
111 }; 101 };
112 }; 102 };
113 const devRender = (route) => { 103 const devRender = (route) => {
114 return async(req, res, next) => { 104 return async(req, res, next) => {
  105 + try {
115 res.setHeader('X-YOHO-Version', pkg.version); 106 res.setHeader('X-YOHO-Version', pkg.version);
116 - const ck = getCacheKey(req.url, route.cackeKey); 107 + const ck = route.cackeKey ? getCacheKey(req.url, route.cackeKey) : void 0;
117 108
118 if (config.useCache && route.cache && ck) { 109 if (config.useCache && route.cache && ck) {
119 const html = await redis.getAsync(ck); 110 const html = await redis.getAsync(ck);
@@ -132,19 +123,12 @@ const devRender = (route) => { @@ -132,19 +123,12 @@ const devRender = (route) => {
132 process.removeListener('message', event); 123 process.removeListener('message', event);
133 if (msg.action === 'ssr_request') { 124 if (msg.action === 'ssr_request') {
134 if (msg.err) { 125 if (msg.err) {
135 - try {  
136 - const err = JSON.parse(msg.err); 126 + let err = msg.err;
137 127
138 - if (err.code === 404) {  
139 - return next();  
140 - }  
141 - return next(err);  
142 - } catch (e) {  
143 - return next({  
144 - code: 500,  
145 - message: msg.err  
146 - });  
147 - } 128 + try {
  129 + err = JSON.parse(msg.err);
  130 + } catch (error) {} // eslint-disable-line
  131 + return handlerError(err, req, res, next);
148 } 132 }
149 if (config.useCache && route.cache && ck) { 133 if (config.useCache && route.cache && ck) {
150 redis.setex(ck, route.cacheTime || 60, msg.html); 134 redis.setex(ck, route.cacheTime || 60, msg.html);
@@ -154,6 +138,9 @@ const devRender = (route) => { @@ -154,6 +138,9 @@ const devRender = (route) => {
154 }; 138 };
155 139
156 process.on('message', event); 140 process.on('message', event);
  141 + } catch (error) {
  142 + return next(error);
  143 + }
157 }; 144 };
158 }; 145 };
159 146
@@ -165,4 +152,6 @@ _.each(routes, r => { @@ -165,4 +152,6 @@ _.each(routes, r => {
165 } 152 }
166 }); 153 });
167 154
168 -module.exports = router; 155 +exports.ssrRender = isDev ? devRender({}) : render({});
  156 +
  157 +exports.routers = router;
  1 +const _ = require('lodash');
  2 +const crypto = global.yoho.crypto;
  3 +
  4 +function urlJoin(a, b) {
  5 + if (_.endsWith(a, '/') && _.startsWith(b, '/')) {
  6 + return a + b.substring(1, b.length);
  7 + } else if (!_.endsWith(a, '/') && !_.startsWith(b, '/')) {
  8 + return a + '/' + b;
  9 + } else {
  10 + return a + b;
  11 + }
  12 +}
  13 +
  14 +function _encode(str) {
  15 + return encodeURIComponent(crypto.encryption(null, str));
  16 +}
  17 +
  18 +const encode = _.memoize(_encode);
  19 +
  20 +function getRouter(req) {
  21 + let route = req.route ? req.route.path : '';
  22 + let appPath = req.app.mountpath;
  23 +
  24 + if (_.isArray(route) && route.length > 0) {
  25 + route = route[0];
  26 + }
  27 +
  28 + let key = urlJoin(appPath, route.toString()); // route may be a regexp
  29 +
  30 + if (key) {
  31 + return encode(key);
  32 + }
  33 +
  34 + return '';
  35 +}
  36 +
  37 +module.exports.md = function(req, res, next) {
  38 + function onRender() {
  39 + res.locals._router = getRouter(req);
  40 + }
  41 +
  42 + res.on('beforeRender', onRender);
  43 + next();
  44 +};
  45 +
  46 +module.exports.getRouter = getRouter;