vue服务端渲染的实例代码

网友投稿 273 2023-04-13


vue服务端渲染的实例代码

一、什么是服务端渲染

客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回Promise (官方是asyncData方法)来将需要的数据拿到。最后再通过window.__initial_state=data将其写入网页,最后将服务端渲染好的网页返回回去。接下来客户端将用新的store状态把原来的store状态替换掉,保证客户端和服务端的数据同步。遇到没被服务端渲染的组件,再去发异步请求拿数据。

服务端渲染的环境搭建

这是vue官网的服务端渲染的示意图,ssr有两个入口文件,分别是客户端的入后文件和服务端的入口文件,webpack通过两个入口文件分别打包成给服务端用的server bundle和给客户端用的client bundle.当服务器接收到了来自客户端的请求之后,会创建一个渲染器bundleRenderer,这个bundleRenderer会读取上面生成的server bundle文件,并且执行它的代码, 然后发送一个生成好的html到浏览器,等到客户端加载了client bundle之后,会和服务端生成的DOM进行Hydration(判断这个DOM和自己即将生成的DOM是否相同,如果相同就将客户端的vue实例挂载到这个DOM上)

实现步骤:

1、创建vue实例(main.js)

importVuefrom'vue'

importAppfrom'./App.vue'

importiViewfrom'iview';

import{createStore}from'./store'

import{createRouter}from'./router'

import{sync}from'vuex-router-sync'

Vue.use(iView);

export functioncreateApp() {

conststore = createStore()

constrouter = createRouter()

sync(store,router)

constapp =newVue({

router,

store,

render: h => h(App)

})

return{app,router,store}

}

因为要做服务端渲染,所以这里不需要再用el去挂载,现将app、router、store导出

2、服务端入口文件(entry-server.js)

import{ createApp }from'./main'

constisDev = process.env.NODE_ENV !=='production'

const{ app,router,store } = createApp()

constgetAllAsyncData=function(component){

letstores = []

functionloopComponent(component) {

if(typeofcomponent.asyncData !=='undefined') {

for(letaofcomponent.asyncData({store,route: router.currentRoute})) {

stores.push(a)

}

}

if(typeofcomponent.components !=='undefined') {

for(letcincomponent.components){

loopComponent(component.components[c])

}

}

}

loopComponent(component)

returnstores

}

export defaultcontext => {

return newPromise((resolve,reject) => {

consts = isDev && Date.now()

const{url} = context

constfullPath = router.resolve(url).route.fullPath

if(fullPath !== url) {

reject({url: fullPath })

}

router.push(url)

router.onReady(() => {

constmatchedComponents = router.getMatchedComponents()

if(!matchedComponents.length) {

reject({code:404})

}

letallAsyncData = getAllAsyncData(matchedComponents[0])

Promise.all(allAsyncData).then(() => {

isDev && console.log(`data pre-fetch:${Date.now() - s}ms`)

context.state = store.state

resolve(app)

}).catch(reject)

},reject)

})

}

这个文件的主要工作是接受从服务端传递过来的context参数,context包含当前页面的url,用getMatchedComponents方法获取当前url下的组件,返回一个数组,遍历这个数组中的组件,如果组件有asyncData钩子函数,则传递store获取数据,最后返回一个promise对象。

store.state的作用是将服务端获取到的数据挂载到context对象上,后面在server.js文件里会把这些数据直接发送到浏览器端与客户端的vue实例进行数据(状态)同步。

3、客户端入口文件(entry-client.js)

importVuefrom'vue'

import'es6-promise/auto'

import{ createApp }from'./main'

importProgressBarfrom'./components/ProgressBar.vue'

// global progress bar

constbar = Vue.prototype.$bar =newVue(ProgressBar).$mount()

document.body.appendChild(bar.$el)

Vue.mixin({

beforeRouteUpdate(to,from,next) {

const{ asyncData } =this.$options

if(asyncData) {

Promise.all(asyncData({

store:this.$store,

route: to

})).then(next).catch(next)

}else{

next()

}

}

})

const{ app,router,store } = createApp()

if(window.__INITIAL_STATE__) {

store.replaceState(window.__INITIAL_STATE__)

}

router.onReady(() => {

router.beforeResolve((to,from,next) => {

constmatched = router.getMatchedComponents(to)

constprevMatched = router.getMatchedComponents(from)

letdiffed =false

constactivated = matched.filter((c,i) => {

returndiffed || (diffed = (prevMatched[i] !== c))

})

constasyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)

if(!asyncDataHooks.length) {

returnnext()

}

bar.start()

Promise.all(asyncDataHooks.map(hook => hook({ store,route: to })))

.then(() => {

bar.finish()

next()

})

.catch(next)

})

app.$mount('#app')

})

if('https:'=== location.protocol && navigator.serviceWorker) {

navigator.serviceWorker.register('/service-worker.js')

}

if(window.INITIAL_STATE) {

store.replaceState(window.INITIAL_STATE)

}

这句的作用是如果服务端的vuex数据发生改变,就将客户端的数据替换掉,保证客户端和服务端的数据同步

Service Worker主要用于拦截并修改访问和资源请求,细粒度地缓存资源。它运行浏览器在后台,运行环境与普通页面脚本不同,所以不能直接参与页面交互。出于安全考虑,service worker只能运行在HTTPS上,防止被人从中攻击。

4、创建服务端渲染器(server.js)

constfs = require('fs')

constpath = require('path')

constLRU = require('lru-cache')

constexpress = require('express')

constcompression = require('compression')

constresolve= file => path.resolve(__dirname,file)

const{ createBundleRenderer } = require('vue-server-renderer')

constisProd = process.env.NODE_ENV ==='production'|| process.env.NODE_ENV ==='beta'

constuseMicroCache = process.env.MICRO_CACHE !=='false'

constserverInfo =

`express/${require('express/package.json').version}`+

`vue-server-renderer/${require('vue-server-renderer/package.json').version}`

constapp = express()

consttemplate = fs.readFileSync(resolve('./src/index.template.html'),'utf-8')

functioncreateRenderer(bundle,options) {

returncreateBundleRenderer(bundle,Object.assign(options,{

template,

cache: LRU({

max:1000,

maxAge:1000*60*15

}),

basedir: resolve('./dist'),

runInNewContext:false

}))

}

letrenderer

letreadyPromise

if(isProd) {

constbundle = require('./dist/vue-ssr-server-bundle.json')

constclientManifest = require('./dist/vue-ssr-client-manifest.json')

renderer = createRenderer(bundle,{

clientManifest

})

}else{

readyPromise = require('./build/setup-dev-server')(app,(bundle,options) => {

renderer = createRenderer(bundle,options)

})

}

constserve= (path,cache) => express.static(resolve(path),{

maxAge: cache && isProd ?1000*60*60*24*30:0

})

app.use(compression({threshold:0}))

app.use('/dist',serve('./dist',true))

app.use('/static',serve('./static',true))

app.use('/service-worker.js',serve('./dist/service-worker.js'))

constmicroCache = LRU({

max:100,

maxAge:1000

})

constisCacheable= req => useMicroCache

functionrender(req,res) {

consts = Date.now()

res.setHeader("Content-Type","text/html")

res.setHeader("Server",serverInfo)

consthandleError= err => {

if(err.url) {

res.redirect(err.url)

}else if(err.code ===404) {

res.status(404).end('404 | Page Not Found')

}else{

// Render Error Page or Redirect

res.status(500).end('500 | Internal Server Error')

console.error(`error during render :${req.url}`)

console.error(err.stack)

}

}

constcacheable = isCacheable(req)

if(cacheable) {

consthit = microCache.get(req.url)

if(hit) {

if(!isProd) {

console.log(`cache hit!`)

}

returnres.end(hit)

}

}

constcontext = {

title:'Vue DB',// default title

url: req.url

}

renderer.renderToString(context,(err,html) => {

if(err) {

returnhandleError(err)

}

res.end(html)

if(cacheable) {

microCache.set(req.url,html)

}

if(!isProd) {

console.log(`whole request:${Date.now() - s}ms`)

}

})

}

app.get('*',isProd ? render : (req,res) => {

readyPromise.then(() => render(req,res))

})

constport = process.env.PORT ||8888

app.listen(port,() => {

console.log(`server started at localhost:${port}`)

})

5、客户端api文件create-api-client.js

/**

* Created by lin on 2017/8/25.

*/

import axios from 'axios';

let api;

axios.defaults.baseURL = process.env.API_URL;

axios.defaults.timeout = 10000;

axios.interceptors.response.use((res) => {

if (res.status >= 200 && res.status < 300) {

return res;

}

return Promise.reject(res);

}, (error) => {

return Promise.reject({message: '网络异常,请刷新重试', err: error});

});

if (process.__API__) {

api = process.__API__;

} else {

api = {

get: function(url) {

return new Promise((resolve, reject) => {

axios.get(url).then(res => {

resolve(res);

}).catch((error) => {

reject(error);

});

});

},

post: function(target, options = {}) {

return new Promise((resolve, reject) => {

axios.post(target, options).then(res => {

resolve(res);

}).catch((error) => {

reject(error);

});

});

}

};

}

export default api;

6、服务端api文件create-api-server.js

/**

* Created by lin on 2017/8/25.

*/

import axios from 'axios';

let cook = process.__COOKIE__ || '';

let api;

axios.defaults.baseURL = 'https://api.douban.com/v2/';

axios.defaults.timeout = 10000;

axios.interceptors.response.use((res) => {

if (res.status >= 200 && res.status < 300) {

return Promise.resolve(res);

}

return Promise.reject(res);

}, (error) => {

// 网络异常

return Promise.reject({message: '网络异常,请刷新重试', err: error, type: 1});

});

if (process.__API__) {

api = process.__API__;

} else {

api = {

get: function(target) {

return new Promise((resolve, reject) => {

axios.request({

url: encodeURI(target),

method: 'get',

headers: {

'Cookie': cook

}

}).then(res => {

resolve(res);

}).catch((error) => {

reject(error);

});

});

},

post: function(target, options = {}) {

return new Promise((resolve, reject) => {

axios.request({

url: target,

method: 'post',

headers: {

'Cookie': cook

},

params: options

}).then(res => {

resolve(res);

}).catch((error) => {

reject(error);

});

});

}

};

}

export default api;

六、那些年遇到的那些坑

问题1、window is not defined

答案1:给用到浏览器对象的地方加if (typeof window !== 'undefined') {},有一些插件里也用到了浏览器对象,在使用的地方也加一个条件判断:

if (typeofwindow !== 'undefined') {

Vue.use(VueAnalytics, {

id: process.env.UA_TRACKING_ID,

router

})

}

问题2:用到非Vue系列的插件,如hello.all.js(三方登录的插件),需要用的地方才引用,报的错和问题1一样。

答案2:这个时候不能再用import导入,需要使用require,

let hello

if (typeof window !== 'undefined') {

hello = require('hello')

}

问题3:引用bootstrap

答案3:将bootstrap.css和bootstrap.js加入webpack.base.config.js的entry中的vendor中

问题6:bootstap需要jquery,此时把jQuery加在vendor中没用。

答案6:给webpack.base.config.js的plugins添加一个插件,如:

newwebpack.ProvidePlugin({

$ : "jquery",

jQuery : "jquery",

"window.jQuery" :"jquery"

})

七、例子

https://github.com/linmoer/ssr-vue这是一个服务端渲的例子


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:dubbo接口测试核心(dubbo接口压测)
下一篇:微服务mock测试工具(微服务测试框架)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~