SSR踩坑集锦

研究SSR距现在有些年头了了,这里总结我当时踩过的和看到的坑,为同样在研究SSR的小伙伴节省时间成本,最重要的是怕自己忘了。。。

直奔主题

使用服务端渲染常见问题:

过滤CSS和图片文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SSR环境对样式文件处理
csshook({
extensions: ['.less', '.css', '.scss'],
processorOpts: {
parser: lessParser
},
generateScopedName: '[name]__[local]___[hash:base64:5]'
});

// SSR环境对图片资源处理
assethook({
extensions: ['jpg', 'png', 'gif', 'webp', 'svg'],
limit: 10000,
name: (file) => {
/* eslint-disable fecs-quotes, quotes*/
let moduleName = file.split('app/')[1].split('/')[0];
const match = '/src/client';
let path = file.substring(file.indexOf(`/client`) + 8, file.lastIndexOf('/'));
return process.env.YOG_ENV === 'dev' ? `/static/${moduleName}/img/${path}/[name].[ext]` : `https://sv.bdstatic.com/static/${moduleName}/img/${path}/[name].[ext]`;
/* eslint-enable fecs-quotes, quotes*/
}
});

或者使用ignore-loader

1
2
3
4
5
6
7
8
9
10
11
12
{
test: /\.css$/,
use: [
'ignore-loader'
]
},
{
test: /\.less$/,
use: [
'ignore-loader'
]
},

客户端展示异常,服务端报错 window/alert/document is undefined

服务端没有window/alert/document这种东西,需要自行定义,建议方式引入第三方包jsdom辅助定义

1
2
3
4
5
6
7
8
9
10
11
12
13
//https://github.com/vuejs/vue-hackernews-2.0/issues/52#issuecomment-255594303
const { JSDOM } = require('jsdom')
const dom = new JSDOM('<!doctype html><html><body></body></html>',
{ url: 'http://localhost' })

global.window = dom.window
global.document = window.document
global.navigator = window.navigator

require('matchmedia-polyfill');
require('matchmedia-polyfill/matchMedia.addListener');

...
或者参考https://www.bountysource.com/issues/42051318-universal-referenceerror-document-is-not-defined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const domino = require('domino');

const window = domino.createWindow('<h1>Hello world</h1>', 'http://example.com');
const document = window.document;
global['window'] = window;
global['document'] = document;
global['DOMTokenList'] = window.DOMTokenList;
global['Node'] = window.Node;
global['Text'] = window.Text;
global['HTMLElement'] = window.HTMLElement;
global['navigator'] = window.navigator;

Object.defineProperty(window.document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
global['CSS'] = null;
// global['XMLHttpRequest'] = require('xmlhttprequest').XMLHttpRequest;
global['Prism'] = null;

const fakeStorage: Storage = {
length: 0,
clear: () => {
},
getItem: (_key: string) => null,
key: (_index: number) => null,
removeItem: (_key: string) => {
},
setItem: (_key: string, _data: string) => {
}
};

(global as any)['localStorage'] = fakeStorage;




...


import { createServer } from 'http';
import { join } from 'path';

import { enableProdMode } from '@angular/core';
import { MODULE_MAP } from '@nguniversal/module-map-ngfactory-loader';
import { NgSetupOptions } from '@nguniversal/express-engine';

import { createApi } from './api';

export { AppServerModule } from './app/app.server.module';

export const PORT = process.env.PORT || 4000;
export const BROWSER_DIST_PATH = join(__dirname, '..', 'browser');

export const getNgRenderMiddlewareOptions: () => NgSetupOptions = () => ({
bootstrap: exports.AppServerModuleNgFactory,
providers: [
// Import module map for lazy loading
{
provide: MODULE_MAP,
useFactory: () => exports.LAZY_MODULE_MAP,
deps: [],
}
]
});

image

引入Antd后matchMedia问题

1
Error: matchMedia not present, legacy browsers require a polyfill

解决办法:

1
cnpm i -S jsdom whatwg-encoding matchmedia-polyfill

具体配置如上图片。

router中配置了scrollBehavior,客户端正常,服务端报错scroll undefined

跟上个问题相同,需要在服务端重声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//fixed Not-implemented error
const isServer = process.env.VUE_ENV === 'server'

if(isServer) {
window.scrollTo = function(x, y) {
// do something or not
}
}

export function createRouter() {
return new Router({
scrollBehavior: () => ({ y: 0 }),
routes: [
{ path: '/', component: Homepage }
]
})
}

mismatch

使用ssr会有检查服务端渲染出的结构与直接客户端渲染的结构是否相同,不同会报mismatch。这种问题往往是因为比如table结构没有tbody之类的。
自己的一些业务操作也可能会产生两端的结构重复,比如我之前为了动态生成meta用了mixin,在服务端用$ssrContext配合操作,客户端则用的document直接更改对应值,因此会出现一个页面有两个重复的meta,造成mismatch,解决方式是在客户端加判断,如果已经有的meta就使用修改而不是增加

1
2
3
4
5
6
if (meta) {
$.parseHTML(meta).forEach(function(el) {
$('meta[name=' + $(el).attr('name') + ']')
.attr('content', $(el).attr('content'))
})
}

区别终端类型

比如在PC端使用a链接作为入口,移动端使用b链接作为入口
客户端:使用navigator.userAgent做判断,然后

1
2
3
4
5
6
7
8
9
Vue.mixin({
beforeRouteEnter(to, from, next) {
if(judgeUserAgent() && to.path === '/a/' ) {
next('/b/')
} else {
next()
}
}
})

服务端: 在server.jsrender中通过req.headers['user-agent']然后通过$ssrContext传递

1
2
3
4
5
if(context.agentID !== null && context.url === '/a/') {
router.push('/b/')
} else {
router.push(context.url)
}

项目不在服务器对应位置的根目录而在二级目录

一般打包都打包到根目录,获取静态文件资源也从/开始,如果不是,怎么办呢?
其实也不难,把各种相关配置更改一下就好了,就是这些位置自己摸索到时候有些麻烦,尤其还是ssr,漏掉就可能造成项目起不来或白屏、报错、刷新404。

我这里列了一下要修改的位置(按文件顺序,假设二级目录名为dev):

  • build/setup-dev-server.js中的webpack-hot-middleware
1
2
3
4
5
clientConfig.entry.app = ['webpack-hot-middleware/client?path=/dec/__webpack_hmr', clientConfig.entry.app]

app.use(require('webpack-hot-middleware')(clientCompiler, {
path: '/dev/__webpack_hmr'
}))
  • webpack.base.config.jsoutput
1
2
3
4
5
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dev/dist/',
filename: '[name].[chunkhash].js'
},
  • entry-client.jsservice worker
1
2
3
if (process.env.NODE_ENV === 'production' && 'serviceWorker'in navigator) {
navigator.serviceWorker.register('/dev/service-worker.js')
}
  • template.htmlhref
1
<linkrel="shortcut icon"href="/dev/assets/images/favicon.ico">
  • server.jsserve
1
2
3
4
app.use(favicon('./src/assets/images/favicon.ico'))
app.use('/dev/dist', serve('./dist', true))
app.use('/dev/assets', serve('./src/assets', true))
app.use('/dev/service-worker.js', serve('./dist/service-worker.js'))

如果是非服务端渲染需要修改config/index.jsassetsPublicPath/dev/

通用:ReferenceError:未定义文档

https://miyalee.github.io/2018/01/03/blog2018-01-03/

matchmedia-polyfill