首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法变现,能坚持下去也是靠的是本身的热情和你们的鼓励。javascript
上一篇文章Vue同构(一)咱们介绍了若是使用Vue同构在服务端渲染一个简单组件并在服务端对应激活。对应的代码已经上传到Github。本篇文章咱们介绍Vue同构中路由相关的知识。html
写到这里咱们首先讨论一下为何会须要有前端路由,为何咱们的程序中须要引入Vue-Router呢?其实最先的网站都是服务器渲染的,并不存在什么浏览器渲染。每次在浏览器导航栏输入对应的URL或者点击当前的页面的连接的时候,浏览器就会接收到对应的URL并渲染出HTML页面。这就会存在一个问题,就是每次操做都意味着页面刷新。异步请求的出现,解决了这一切,咱们能够经过XMLHTTPRequest去动态请求数据而不是每次都刷新对应界面,实现了不须要后台刷新实现页面交互。后来单页面应用(SPA: Single Page Web Application)的出现将这个概念更进一步,不只页面交互不须要刷新页面,连页面跳转都不须要刷新当前页面。当页面跳转都不须要刷新当前页面时,咱们必须就要解决的是不一样URL下组件切换的问题,这也就是前端路由所作的工做。前端
路由(Router)概念实际上是来自于后台,负责URL到函数的映射。好比:vue
/user -> getAllUsers() /user/count -> getUserCount()
其中的每个URL到函数的映射规则咱们称为一个route
,而router
则至关于管理route
的容器。前端路由的概念与此相似,只不过URL映射的是前端组件。好比:java
/user -> User组件
得益于Vue的优雅设计,Vue与Vue Router的结合使用很是简单,其实就是首先配置好路由规则并生成路由实例,而后将路由实例传递给Vue根元素将其添加进来,最后使用router-view
组件来告诉Vue Router在哪里渲染。webpack
<div id="app"> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view> </div>
//引入路由组件 import Home from '../components/Home.vue' import About from '../components/About.vue' //路由配置 const routes = [ { path: '/', component: Home }, { path: '/home', component: Home }, { path: '/about', component: About } ] //建立Vue Router实例 var router = new VueRouter({ routes }) //引入vue-router const app = new Vue({ router, //...... })
上面咱们介绍了Vue Router在客户端渲染的逻辑,固然这只是最简单的逻辑,更高阶的使用能够参阅Vue Router官方文档,并非本篇文章的重点内容,所以咱们就不在赘述。git
Vue Router其实有两种模式: hash
模式和history
模式。hash
模式是Vue Router默认的模式。要讲清这两种模式咱们不得不提到两种模式所对应的不一样的实现逻辑。github
其实咱们能够想到,做为前端路由切换的过程当中是不能引发浏览器刷新的,不然就违反了SPA路由交互的规则。首先咱们就瞄上了URL中的片断标识符(锚点),做为一个完整的URL,格式以下web
http://user:pass@www.example.com:80/dir/index.html?uid=1#ch1
#ch1的部分就是咱们所说的片断标识符,一般可用来标记出已获取资源中的子资源,片断标识符的改变并不会引发浏览器的刷新,所以hash
模式就是使用的片断标识符来做为前端路由的依据。在前端路由中咱们把片断标识符称做hash部分,hash部分仅仅只是客户端的状态,hash部分并不会被服务器端所接收。咱们能够经过window.onhashchagnge
事件来监听url中hash
部分的变化,这也是基于hash路由的基础。举个例子:vue-router
window.addEventListener('hashchange', function(e){ console.log('hashchange', e); })
若是浏览器的hash部分变化了,监听函数会马上调用对应的事件。
HTML5 引入了新的API,能够在不刷新当前页面的状况下,改变URL。分别对应的两个方法:
pushState(state, title, url)
replaceState(state, title, url)
pushState
用于向浏览器的历史记录中添加一条新记录,同时改变地址栏的地址内容。replaceState
则与pushState
相似,是修改了当前的历史记录项而不是新建一个。两个函数对应的参数分别是:
与pushState
和replaceState
配套使用的是onpopstate
事件,须要注意的是调用history.pushState()
或history.replaceState()
不会触发popstate
事件。只有在作出浏览器动做时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()
)
例如:
window.addEventListener('popstate', function(){ console.log("location: " + document.location + ", state: " +JSON.stringify(event.state)); }) history.pushState({page: 1}, "title 1", "?page=1"); history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
两种模式咱们说完了,history
相比于hash
来讲url要美观,可是须要后台服务器的支持,由于history
最怕浏览器刷新了,好比咱们前端的路由从/home
改变为/about
,这个仅仅是前端url的改变,并不会刷新当前页面,而且包括浏览器的后退和前进也不会刷新浏览器。可是若是一旦刷新,浏览器是真的会去请求当前的url,好比/about
。这个时候,若是浏览器并不能识别这个url,就可能找不到当前页面。
说了这么多,咱们服务器渲染须要采用哪一种模式呢?咱们采用的是history
模式,这个是惟一的选择,答案其实上面已经说过了,由于hash部分仅仅只是客户端的状态,并不会被服务器端所接收。如今咱们假设咱们当前的应用有两个路由:
/ -> Home /about -> About
首先咱们建立咱们的路由实例,上一篇文章中咱们会为每次的请求建立新的组件实例,其目的就是为了方式不一样的请求之间交叉影响,路由实例也是相同的道理:
// router/index.js import Vue from 'vue' import Router from 'vue-router' import Home from '../components/Home.vue' import About from '../components/About.vue' Vue.use(Router) export function createRouter() { return new Router({ mode: "history", routes: [{ path: '/', component: Home }, { path: "/about", component: About }] }) }
createRouter
函数每次调用都建立一个路由实例,路由实例中配置的history
模式,而且配置了路由规则。
接下来咱们看看Home
组件:
<template> <div> <div>当前位置: About</div> <router-link to="/home">前往Home</router-link> <button @click="directHome">按钮: 前往Home</button> </div> </template> <script> export default { name: "about", methods: { directHome: function () { this.$router.push('/'); } } } </script>
这个组件我之因此在使用了router-link
的状况下还使用了button
,主要是为了证实客户端已经激活。About
组件和Home
组件除了名字和连接地址不一样,其他彻底一致,再也不列出。
咱们在根组件App
中渲染路由匹配的组件
//App.Vue <template> <div id="app"> <router-view></router-view> </div> </template>
接下来咱们须要继续改造app.js
,上篇文章中咱们已经介绍过服务器中app.js
主要任务是对外暴露一个工厂函数,具体客户端和浏览器端的逻辑已经分别转移到客户端和浏览器端的入口文件entry-client.js
和entry-server.js
import Vue from 'vue' import App from './components/App.vue' import {createRouter} from './router' export function createApp() { const router = createRouter() const app = new Vue({ router, render: h => h(App) }) return { app, router } }
createApp
与以前不一样之处在于,每次建立的Vue实例中都注入了router
。并返回了建立的Vue实例和Vue Router实例。
服务端渲染的逻辑集中在entry-server.js
:
// entry-server.js import { createApp } from './app' export default function (context) { return new Promise((resolve, reject) => { const {app, router} = createApp() router.push(context.url) router.onReady(() => { // Promise 应该 resolve 应用程序实例,以便它能够渲染 resolve(app) }, reject) }) }
entry-server.js
做为服务端渲染的入口打包为对应的bundle
传入createBundleRenderer
生成renderer
,调用renderer.renderToString
能够传入context
,其中就能够包含当前路由的url
。而咱们在entry-server.js
的函数中接受该context
对象即可以获取该路由信息。
与上面文章不一样的,咱们并无直接返回Vue实例而是返回了一个Promise
,在Promise
中首先咱们调用createApp
获取到Vue实例app
和Vue Router实例router
,而后咱们调用push
函数将当前的路由导航到目标url上。而后咱们调用在router.onReady
函数,确保等待路由的全部异步钩子函数和异步组件加载完毕以后,resolve
当前的Vue实例。
与entry-server.js
类似,客户端的打包入口文件entry-client.js
也须要在挂载 app 以前调用 router.onReady
:
import { createApp } from './app' const {app, router} = createApp(); router.onReady(() => { app.$mount('#app') })
如今咱们继续来看咱们的express
服务器代码,和上次的渲染基本彻底一致,只不过咱们须要给renderToString
传递一个context
对象,其中包含当前的url
值便可。
//server.js //省略...... app.get('*', (req, res) => { const context = { url: req.url } renderer.renderToString(context, function (err, html) { res.end(html) }) }) //省略......
如今咱们打包好服务端和浏览器端的bundle
,并启动服务器:
如今咱们思考一个问题,若是咱们设置为路由router
中设置了守卫,是会在浏览器中执行仍是会为服务端执行呢?为了验证这个问题,咱们给router
增长全局守卫beforeEach
与afterEach
:
export function createApp() { const router = createRouter() router.beforeEach((to, from, next) => { console.log("beforeEach---start"); console.log('to: ', to.path, ' from: ', from.path); console.log("beforeEach---end"); next(); }) router.afterEach((to, from) => { console.log("afterEach---start"); console.log('to: ', to.path, ' from: ', from.path); console.log("afterEach---end"); }) // 省略...... }
咱们直接访问/
路由,咱们能够看到服务端和客户端的输出结果以下:
这说明守卫函数在服务器端和客户端都同时执行了,两端的路由都解析了调用组件中可能存在的路由钩子。开发过程当中可能要留心这点。
首先能够考虑一个问题,咱们当初引入Vue的同构的主要目的就是加快首屏的显示速度,那么咱们能够考虑一下,若是咱们访问/
路由的时候,其实只须要加载Home
组件就能够了,并不须要加载About
组件。等到须要的时候,咱们能够再去加载About
组件,这样咱们就能够减小初始渲染中下载的资源体积,加快可交互时间。在这里咱们就能够考虑对代码进行分割。
代码分割其实也是Webpack所支持的特性,能够将不一样的代码打包到不一样的bundle
中,而后按需加载文件。
Webpack
最简单的代码分割无非是手动操做,你能够经过配置多个entry
来实现,可是手动的模式存在诸多的问题,好比多个bundle
都引用了相同的模块,则每一个bundle
中都存在重复代码。这个问题却是好解决,咱们可使用SplitChunksPlugin
插件去解决这个问题。可是手动毕竟仍是不太方便,因此Webpack提供了更为方便的动态导入。
动态导入的功能推荐使用ECMAScript提案的import()
语法,import()
能够指定所要加载的模块的位置,而后执行时动态加载该模块,并返回一个Promise
。好比说咱们在一个模块中想要动态加载lodash
模块,咱们首先能够在Webpack的配置文件中添加:
output: { chunkFilename: '[name].bundle.js', },
chunkFilename
就是为了配置决定非入口chunk的名称,而后在代码中:
import(/* webpackChunkName: "lodash" */ 'lodash').then(lodash => { //lodash即可以使用 })
打包代码咱们能够发现lodash被单独打包,由于在注释中咱们将webpackChunkName的值赋值为lodash,所以将其命名为 lodash.bundle.js。固然这种chunkFilename
也并非必须的,默认会被命名成[id].bundle.js
。
Vue提供异步组件的概念,容许咱们将代码分割成代码块,而且按需加载。相比与普通的组件注册,咱们能够用工厂函数的方式定义组件,这个工厂函数会收到一个resolve
回调,这个回调函数会在你从服务器获得组件定义的时候被调用。或者直接在该工厂函数中返回一个Promise
。咱们知道import()
语法返回的就是一个Promise
,所以咱们搭配改造以前的代码:
// router.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export function createRouter() { return new Router({ mode: "history", routes: [{ path: '/', component: () => import('../components/Home.vue') }, { path: "/about", component: () => import('../components/About.vue') }] }) }
而后打包客户端bundle:
> vue-ssr-demo@1.0.0 build:client /Users/mr_wang/WebstormProjects/vue-ssr-demo > cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules Hash: 16fbba9bf008ec7ef466 Version: webpack 3.12.0 Time: 1158ms Asset Size Chunks Chunk Names 0.8ac6ad83b93d774d3817.js 5.04 kB 0 [emitted] 1.5967060b78729a4577f9.js 5.04 kB 1 [emitted] app.1c160fc3e08eec3aed0f.js 7.37 kB 2 [emitted] app vendor.f32c57c9ee5145002da1.js 296 kB 3 [emitted] [big] vendor manifest.4b057fd51087adaec1f3.js 5.85 kB 4 [emitted] manifest vue-ssr-client-manifest.json 1.48 kB [emitted]
咱们发现输出文件多了0.[hash].js和1.[hash].js,其中分别对应的就是Home
组件与About
组件。固然若是你以为这个模块看起来不清晰,也能够按照以前所说的传入webpackChunkName参数,让打包出来的问题更具备可识别性:
component: import(/* webpackChunkName: "home" */'../components/Home.vue')
component: import(/* webpackChunkName: "about" */'../components/About.vue')
这时Webpack打包出的文件:
Hash: aaf79995904c4786cadc Version: webpack 3.12.0 Time: 976ms Asset Size Chunks Chunk Names home.bundle.js 5.04 kB 0 [emitted] home about.bundle.js 5.04 kB 1 [emitted] about app.f22015420ff0db6ec4b0.js 7.37 kB 2 [emitted] app vendor.f32c57c9ee5145002da1.js 296 kB 3 [emitted] [big] vendor manifest.2a21c55e4a3e98ab252c.js 5.83 kB 4 [emitted] manifest vue-ssr-client-manifest.json 1.44 kB [emitted]
而后咱们启动服务器,访问'/'路由,咱们发现请求以下:
首先咱们看network,咱们发现,0.[hash].js
首先被请求,而后再请求1.[hash].js
,而且两者加载的优先级是不一样的,0.[hash].js
的优先级高于1.[hash].js
,这是为何呢?咱们看对应的html。
咱们能够看到0.[hash].js
在注入的时候是preload而1.[hash].js
注入的时候是prefetch,preload和prefetch之间有什么区别吗,其实但要说这两个都能单写一篇文章,可是在这边咱们仍是简单总结一下。
prefetch是一种告诉浏览器获取一项可能被下一页访问所须要的资源方式。这意味着资源将以较低优先级地获取,所以prefetch是用于获取非当前页面使用的资源。
preload是告诉浏览器提早加载较晚发现的资源。有些资源是隐藏在CSS和JavaScript中的,浏览器不知道页面即将须要这些资源,而等到发现时加载又太晚了,所以声明式的提早加载。
这篇文章主要讲了在Vue经过下若是使用路由而且如何经过代码分割的方式进一步提升页面首屏加载速度。具体的代码能够点这里查看。最后但愿能点个Star支持一下个人博客,感激涕零,若是有表述错误的地方,欢迎你们指正。