最近抽出了点时间,弄了下vue ssr项目,至于ssr的优点就不多提了。学习路线参照了官方实例,有兴趣的同学可以去看下。
我的项目地址,主要使用了ssr+typescript+vuex+vue-cli 2.0,有兴趣的同学,欢迎start。
那么就先讲下前期的打包配置吧,本地开发,也就是所谓的dev,需要热更新等一系列便于调试的插件,所以需要区分webpack的配置。代码就不多提了,可以看下官方配置,也可以看下我的配置。
如果是使用js+vue-cli 2.0的同学,那么官方实例可以完美支持,一点都不需要动。我用的是ts+vue-cli 2.0写的,webpack 4.0以上才支持ts,所以需要升级webpack版本,但是4.0以后,有很多插件都弃用了,坑的一批。比如压缩css的插件ExtractTextPlugin,需要替换成MiniCssExtractPlugin,但是坑比的是服务端渲染还不能用(document is no defined),这里也是需要注意的点,千万不要在服务端和客户端都能触发的钩子中操作dom,比如created,asyncData。所以不能像之前那样写在base.config里面了,也就是服务端不能使用,如果你也碰到了这个问题,可以看下我的这篇从webpack 3.0升级到4.0的经历。
server.js
项目的起始点就是server.js文件,看下package.json,script命令就可以看出来,其实启动项目就是运行node server.js。官方实例用的那些缓存插件就不多提了,其实有些缓存配置可以配在nginx里面的。细心的同学一看app.all()就知道了,其实这就是开了个node服务器而已,我们前端跳转的路由,就相当于一个get请求,服务器接到这个请求,会根据vue提供的ssr插件,把页面渲染好之后再发送到客户端。在渲染的同时,会有个上下文对象context,记录这你想要往客户都传输的信息,什么都可以传,比如页面渲染时间,语言版本等等。
function render (req, res) {
const s = Date.now()res.setHeader("Content-Type", "text/html");
res.setHeader("Server", serverInfo);
// 往响应头里添加一些服务端信息const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if(err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
res.status(500).send('500 | Internal Server Error')
console.error(`error during render : ${req.url}`)
console.error(err.stack)
}
}const context = {
title: 'Confession-Wall',
url: req.url
}renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
res.send(html);
if (!isProd) {
console.log(`页面渲染耗时: ${Date.now() - s}ms`);
}
})
}app.all(`${config.BasePath}*`, isProd ? render : (req, res) => {
if (req.method !== 'GET') return next();
readyPromise.then(() => render(req, res))
})const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`server started at localhost:${port}`);
})
main.ts
下图为main.ts的代码,由于为了每个用户从服务端拿到的是新的没有污染的代码,所以store,router,vue,每次都要new一个新的实例。
// main.ts
import Vue from 'vue';
import App from './App.vue';
import LocalStore from './store/index'
import LocalRouter from './router/index'
// 在服务器端渲染时把当前的路由信息,同步进store中,也就相当于vuex store中多了个route module
import { sync } from 'vuex-router-sync';
Vue.directive('focus', {
inserted: function (el) {
el.focus();
}
});
export function createApp () {
const store = new LocalStore();
const router = new LocalRouter();
sync(store, router);
const app = new Vue({
router,
store,
render: h => h(App)
})
return { app, router, store };
}
vuex-router-sync
至于使用了vuex-router-sync插件的效果如何,我们可以在浏览器的console里可以打印出来,因为服务端向客户端同步数据,是通过向window全局中注入一个对象,用来记录服务端的store信息。如下图所示,你会看到store里面会多了个route的module,不是必须的,你也可以不是使用,或者通过你自己的方式实现。
文章图片
当然从服务端能带过来的不仅是store,也不仅仅往store里添加route信息,只要你需要的任何骚操作信息都可以,下面会讲到。
entry-server.ts
接下来应该到了entry-server.ts文件了,注释里写了我在学习时对其的理解。
// entry-server
import { createApp } from './main';
export interface Context {
title: string;
url: string;
state: any
}// context由server.js中注入
export default (context: Context) => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()const { url } = context
const { fullPath } = router.resolve(url).route// 判断req里的请求地址是否等于当前路由
if (fullPath !== url) {
return reject({ url: fullPath })
}// 如果等于,则把当前url,push进router中,便于客户端接管
router.push(url)router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 如果路由匹配,则触发服务器端asyncData钩子,此钩子便是你组件定义的钩子函数,
// 默认写在与methods同级,所以取的是其options,其实可以自行定义其位置,和实现方法
// 可以在这里对钩子重写,使之拥有更多功能
Promise.all(matchedComponents.map((Component:any) => {
if (Component.options.asyncData) {
return Component.options.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 把服务端请求到的数据,注入windows中的__INITIAL_STATE__中,便于客户端接管vuex store
context.state = store.state;
resolve(app);
}).catch(reject);
}, reject)
})
}
需要注意的地方是这里暴露出来的方法,返回的是一个promise,之前写的时候不注意,踩了个大坑。Component.options.asyncData,这里其实可以自由发挥的,按官方那个实例来看,一般asyncData钩子是与methods同级,所以这里你去拿组件上的asyncData就可以了。至于叫不叫asyncData,你可以自行发挥,你也可以放在methods里面,怎么样的行。只要在这里能取到相应地方的相应方法就可以了。传入的参数你也可以自行发挥,比如传如isServer: true,用以区分是服务端渲染还是客户端渲染触发了这个钩子,以及重定向方法之类的。context.state就是向客户端注入的内容,可以自行添加东西。比如:
context.state ={
store: store.state,
text: '我是服务端注入的内容'
}
【vue|分享vue项目的服务端渲染学习过程】此时客户端接受的window.__INITIAL_STATE__就如图所示,这是你客户端同步状态的时候就要取对store了。
文章图片
entry-client.ts
接下来应该就是entry-client.ts文件了。
import Vue from 'vue';
import 'es6-promise/auto';
import { createApp }from './main';
import { Route } from 'vue-router';
/**
* 当组件复用时,触发asyncData钩子,重新请求数据
*/
Vue.mixin({
beforeRouteUpdate (to: any, from: any, next: any) {
const { asyncData } = (this as any).$options
if (asyncData) {
asyncData({
store: (this as any).$store,
route: to
}).then(next).catch(next)
} else {
next()
}
}
})const { app, router, store } = createApp()// 获取服务端渲染时,注入的__INITIAL_STATE__信息,并同步到客户端的vuex store中
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}router.onReady(() => {
router.beforeResolve( async (to: Route, from: Route, next: any) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
let diffed = false
// 校验to的路由地址和from的路由地址是否相等,如果不相等则在客户端触发asyncData钩子
const activated = matched.filter((c: any, i: any) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
const asyncDataHooks = activated.map((c:any) => c.options.asyncData).filter((_: any) => _)
if (!asyncDataHooks.length) {
return next()
}
await Promise.all(asyncDataHooks.map( async (hook: any) => await hook({ store, route: to })))
.then(() => {
next()
})
.catch(next)
})
app.$mount('#app');
// 挂在到app上
})// 如果浏览器支持serviceWorker则注册
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js').then((registration) => {
console.log('serviceWorker注册成功')
}).catch(() => {
console.log('serviceWorker注册失败')
})
}
// 向window type中插入__INITIAL_STATE__以至于ts不报错
declare global {
interface Window {
__INITIAL_STATE__: any
}
}
index.template.html
跟官方实例一样,也是可以改造的,你可以在server.js的context中注入你任何想注入的类容,像下面{{ title }}一样注入到你渲染后的模板中,比如页面构建时间,当前语言版本,等一系列操作。body中没东西,就会默认在window中注入名为__INITIAL_STATE__的对象,当然你也可以自定义,比如使用名字为__INIT_STATE__的对象,可以在body中加入这段代码
{{{ renderState({ windowKey: '__INIT_STATE__', contextKey: 'state', }) }}} {{{ renderScripts() }}}
{{ title }} - 锐客网
.vue文件
如果你前面拿的是与methods同级的属性,那么就写在同级就行了,钩子函数的名字和参数与你前面entry-client.ts里保持一致,跟客户端渲染的created钩子差不多,里面放一些请求,和改变vuex的东西进行数据预取。至于vuex的形式,以及用不用vuex做状态管理都无所谓。
async asyncData({store, route}:any) {
const id = route.query.id;
let params: Detail.ArticDetail.RequestParams = {
id: id
};
store.commit('detail/articDetail/$assignParams', params);
await store.dispatch('detail/articDetail/getArticDetail');
}
拿之前的老项目重构的,由于vue-cli 2.0用起来不太好,以及当初自己摸索的vuex写法比较恶心,就只搭个实例了,项目地址,有兴趣的,觉得写了这么多废话有点用的同学欢迎start。
有兴趣的同学,可以一起讨论,无时不在。
推荐阅读
- JavaScript|vue 基于axios封装request接口请求——request.js文件
- JavaScript|JavaScript — 初识数组、数组字面量和方法、forEach、数组的遍历
- JavaScript|JavaScript — call()和apply()、Date对象、Math、包装类、字符串的方法
- 前端|web前端dya07--ES6高级语法的转化&render&vue与webpack&export
- 前端开发|Vue2.x API 学习
- vue|Vue面试常用详细总结
- vue|电商后台管理系统(vue+python|node.js)
- 腾讯TEG实习|腾讯实习——Vue解决跨域请求
- Vue|vue-router 详解
- vue|vue3替代vuex的框架piniajs实例教程