目前做的商城项目处于优化阶段,客户提出SEO优化,让我们给出ssr服务端渲染的方案;此时我的内心是拒绝的;项目做了几年,做完了弄服务端渲染?伊克斯扣斯密?用nuxt吗?重新开发?哦不!怎样增量的嵌入式的修改现有的代码,能做到服务端渲染,是我这段时间来苦心研究的目标;虽然目前客户没有提这个需求了,也是为自己做一个技术储备吧,下面记录我是如何跟着官网给出的文档一步步走下去,实现了一个ssr的小demo的
vue官方ssr指南:https://ssr.vuejs.org/zh/
思路
所谓服务端渲染,个人理解就是数据是从后端获取,然后前端访问url,后端把获取的数据插入到html中然后直接给前端返回HTML页面,这样有利于搜索引擎优化,能够被浏览器收录;同时也加快了页面的打开时间,降低白屏时间,带来更好的用户体验。
第一步:webpack快速搭建vue项目&进行改造
本着从零开始,再一次重新认识webpack的心态,没有用vue-cli搭建项目;毕竟后期如果是真的要改造项目的话,还是要对webpack有比较深入的认识。
上图是Vue官方ssr原理的介绍图,从这张图我们可以知道,最后webpack打包后会生成两个bundle文件,这两个文件分别作用于不同的渲染。
- Client Bundle:用于浏览器渲染,这个就是我们正常项目的普通打包
- Server Bundle:用于服务端渲染
vue项目做ssr,不管是用脚手架搭建还是自己纯手工搭建,都需要用到vue-server-renderer这个库
下面是手工搭建的项目结构图:
webpack.config.js: webpack打包的基础配置
webpack.client.conf.js: 浏览器渲染打包配置
webpack.server.conf.js: ssr打包配配置
index.ssr.html: 顾名思义这个是用来做ssr的
server.js: 是服务端的配置
entry-client.js: 客户端打包入口
entry-server.js: 服务端打包入口
这里我把index.html拆分成了两个,这样可以区别出ssr,进行单独的配置。
index.ssr.html
<body>
<div id="app">
<!--vue-ssr-outlet--> ssr
</div>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
webpack.config.js主要配置:
···
const config = {
// entry: path.join(__dirname, 'src/app.js'),
output: {
filename: '[name].bundle.js',
publicPath: '/', // history模式刷新报错
path: path.join(__dirname, '/dist')
},
resolve: {
alias: {
vue: 'vue/dist/vue.js'
},
extensions: ['.vue', '.ts', '.tsx', '.js', '.json']
},
plugins: [...plugins],
module: {
rules:[...rules],
}
}
if(isDev){
config.devtool = '#cheap-module-eval-source-map'
config.devServer = {
port: 8005,
host: '0.0.0.0',
overlay: {
errors: true // 将webpack编译的错误显示在网页上面
},
open: true // 在启用webpack-dev-server时,自动打开浏览器
}
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
}
module.exports = config;
webpack.client.conf.js核心配置:
···
module.exports = merge(base, {
entry: {
client: './entry-client.js',
},
plugins: [
new HTMLWebpackPlugin({
template: './index.html',
files: {
js: '/client.bundle.js',
},
filename: 'index.html',
}),
]
})
webpack.server.conf.js核心配置:
···
module.exports = merge(base, {
target: 'node',
entry: {
server: './entry-server.js',
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
},
plugins: [
new HtmlWebpackPlugin({
template: './index.ssr.html',
filename: 'index.ssr.html',
files: {
js: '/client.bundle.js',
},
excludeChunks: ['server']
}),
]
})
上面这三个是主要的webpack的配置,主要是做了入口的分别打包处理,分别用client.js和server.js作为入口;这两个就是我们正常项目app.js或者main.js中打包的入口文件
entry-server.js和entry-client.js区别:
entry-client.js是在浏览器端执行,需要挂载dom,启动浏览器渲染,所以需要手动的调用$mount()方法。
entry-server.js是在服务端调用,因此需要导入一个函数,返回一个vue的实例。
export default function createApp() {
const app = new Vue({
render: h => h(App)
});
return app;
};
关于 webpack.server.conf.js,有两个注意点:
libraryTarget: 'commonjs2' → 因为服务器是 Node,所以必须按照 commonjs 规范打包才能被服务器调用。
target: 'node' → 指定 Node 环境,避免非 Node 环境特定 API 报错,如 document 等。
编写服务端渲染主要逻辑
- Vue SSR 依赖于包 vue-server-render,它的调用支持两种入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 组件为入口,后者以打包后的 JS 文件为入口,这里采用第二种。
server.js:
server.use(express.static('dist'));
// server.js 服务端渲染主体逻辑
// dist/server.js 就是以 entry-server.js 为入口打包出来的 JS
const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')
});
server.get('*', (req, res) => {
console.log(req.url)
const context = { url: req.url, pageTitle: 'default-title' }
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
if (err) {
console.log(err);
res.status(500).end('服务器内部错误');
return;
}
res.status = 200
res.type = 'text/html; charset=utf-8'
res.body = html
res.end(html);
resolve(html);
})
});
})
server.listen(8002, () => {
console.log('后端渲染服务器启动, 端口为:8002');
})
- 两个打包入口(entry),重构app, store, router, 为每个对象增加工厂方法createXXX
每个用户通过浏览器访问Vue页面时,都是一个全新的上下文,但在服务端,应用启动后就一直运行着,处理每个用户请求的都是在同一个应用上下文中。为了不串数据,需要为每次SSR请求,创建全新的app, store, router。
index.js核心代码
// createApp工厂方法
export default function (ssrContext) {
const store = createStore(); // 创建全新store实例
const router = createRouter();
// 创建Vue应用
const app = new Vue({
store,
router,
ssrContext,
render: (h) => h(App)
})
return { app, store, router }
}
router.js创建router工厂函数
export function createRouter () {
return new VueRouter({
mode: 'history',
fallback: false,
routes: [
{
path: '/index',
name: 'index',
component: Index
},
]
})
}
store.js 创建store工厂函数
export default function createStore() {
return new Vuex.Store({
state: {
detail: '',
},
getters: {
getDetail(state) {
return state.detail;
}
},
mutations: {
detail(state, arg) {
state.detail = arg;
console.log(state)
}
},
actions: {
getDetail({commit}, payload) {
var p = new Promise((resolve) => {
setTimeout(() => {
resolve({data: '我是数据'})
})
})
// action必须返回promise
return p.then(data => {
console.log(data);
commit('detail', data.data);
})
}
}
})
entry-client.js:
// 创建所需要的app实例
const { app, router, store } = createApp();
// 当路由加载完后挂载dom,渲染
router.onReady(() => {
// 将Vue实例挂载到dom中,完成浏览器端应用启动
app.$mount('#app')
})
entry-server.js:
export default function (context) {
return new Promise((resolve, reject) => {
const { app, router, store} = createApp(context);
// 设置路由
router.push(context.url)
router.onReady(() => {
context.state = store.state;
resolve(app);
})
}
做到这里基本的ssr的工作已经完成了,使用npm run build打包,然后node server.js启动服务,在浏览器访问localhost:8002,就能看到效果啦~完美!访问首localhost:8002/index在network中可以看到返回html中有提前插入的数据;而普通的是没有任何内容的只有<div id="app"></div>
-
ssr:
-
普通:
总结
这篇主要讲了项目的搭建的一些webpack配置,服务端代码,多入口配置以及router,store,app的改造;在这个过程中主要碰到几个问题:
- 详情页面:localhost:8002/detail/1,跳转正常,刷新页面报错。查看network,发现刷新后会去加载localhost:8002/detail/1/client.boundle.js;而我们的这个js文件是与index.ssr.html同级的, 改publicPath:'./'为publicPath:'/'。
-
启动服务之后,打开url,页面一直在请求
服务端没有返回html;在服务端,express框架在拦截到所有路由之后,查阅了别人的处理返回一个promise,然后再resolve(html)就可以了; 我也照做了,但是浏览器会出现一直转圈圈,解决办法在resolve(html)之前加上res.end(html)就好了。
这篇初步试探ssr到这里就告一段落了,下一篇记录下,如何将异步的数据插入到页面中~