vue全家桶知識記錄,只作記錄不深刻原理。
官網都有爲何還要再寫一遍?
爲了熟悉語法而且每一個知識點儘可能用一個簡單例子說明用法
javascript
一個實例從建立到銷燬,在每一個特定的時間都會自動觸發某個函數,這種函數就叫作生命週期函數,也叫鉤子,vue實例有哪些鉤子呢?
css
//組件實例剛被建立,組件屬性計算以前,如data屬性等 beforeCreate //組件實例建立完成,屬性已綁定,但DOM還未生成,$el屬性還不存在 created //模板編譯 / 掛載以前 beforeMount //模板編譯 / 掛載以後 mounted //組件更新以前 beforeUpdate //組件更新以後 updated //組件被激活時調用 activated //組件被移除時調用 deactivated //組件銷燬前調用 beforeDestory //組件銷燬後調用 destoryed
每一個鉤子的觸發時間是什麼?網上找了個圖:
html
寫點代碼測試一下:
前端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app">{{msg}}</div> <script> var app = new Vue({ el:'#app', data:{ msg:'hello' }, beforeCreate(){ console.group('beforeCreate 建立前狀態 ------------>'); console.log("%c%s", "color:red" , "el : " + this.$el); //undefined console.log("%c%s", "color:red","data : " + this.$data); //undefined console.log("%c%s", "color:red","message: " + this.message) }, created() { console.group('created 建立完畢狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); //undefined console.log("%c%s", "color:red","data : " + this.$data); //已被初始化 console.log("%c%s", "color:red","message: " + this.message); //已被初始化 }, beforeMount() { console.group('beforeMount 掛載前狀態 ------------>'); console.log("%c%s", "color:red","el : " + (this.$el)); //已被初始化 console.log("%c%s", "color:red","data : " + this.$data); //已被初始化 console.log("%c%s", "color:red","message: " + this.message); //已被初始化 }, mounted() { console.group('mounted 掛載結束狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); //已被初始化 console.log(this.$el); console.log("%c%s", "color:red","data : " + this.$data); //已被初始化 console.log("%c%s", "color:red","message: " + this.message); //已被初始化 }, beforeUpdate() { console.group('beforeUpdate 更新前狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); console.log(this.$el); console.log('真實dom結構:' + document.getElementById('app').innerHTML); console.log("%c%s", "color:red","data : " + this.$data); console.log("%c%s", "color:red","message: " + this.message); }, updated() { console.group('updated 更新完成狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); console.log(this.$el); console.log('真實dom結構:' + document.getElementById('app').innerHTML); console.log("%c%s", "color:red","data : " + this.$data); console.log("%c%s", "color:red","message: " + this.message); }, beforeDestroy() { console.group('beforeDestroy 銷燬前狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); console.log(this.$el); console.log("%c%s", "color:red","data : " + this.$data); console.log("%c%s", "color:red","message: " + this.message); }, destroyed() { console.group('destroyed 銷燬完成狀態 ------------>'); console.log("%c%s", "color:red","el : " + this.$el); console.log(this.$el); console.log("%c%s", "color:red","data : " + this.$data); console.log("%c%s", "color:red","message: " + this.message) } }) </script> </body> </html>
在官網,vue組件的知識比較分散,這裏把它集合起來,包括組件基礎,組件傳值,動態/異步組件,首先是組件基礎,組件能夠分爲局部組件和全局組件,,具體寫法以下:
vue
//全局組件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <button-counter></button-counter> </div> <script> Vue.component('button-counter',{ template:`<button @click="count++">You clicked me {{ count }} times.</button>`, data:function(){ return { count:0 } } }) var app = new Vue({ el:'#app' }) </script> </body> </html>
全局註冊的行爲必須在根 Vue 實例 ,模板template中只能有一個根元素
java
//局部組件 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <button-counter></button-counter> </div> <script> var buttonCounter = { template:`<button @click="count++">You clicked me {{ count }} times.</button>`, data:function(){ return { count:0 } } } var app = new Vue({ el:'#app', components:{ buttonCounter } }) </script> </body> </html>
接着是組件傳值,分紅父子組件傳值,子父組件傳值,同級組件傳值和複雜組件傳值,首先來看父子組件傳值:
node
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <info :msg="msg"></info> </div> <script> Vue.component('info',{ template:`<div>{{this.msg}}</div>`, props:['msg'] }) var app = new Vue({ el:'#app', data:{ msg:'I am from father component' } }) </script> </body> </html>
接着是子父組件傳值:
webpack
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <info @child="accept"></info> </div> <script> Vue.component('info',{ template:` <div> <button @click="send">click</button> </div> `, data(){ return{ msg:'I am from child component' } }, methods:{ send:function(){ this.$emit('child',this.msg) } } }) var app = new Vue({ el:'#app', methods:{ accept:function(msg){ alert(msg) } } }) </script> </body> </html>
再來寫同級傳值:
git
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <info @child="accept"></info> <msg :value="value"></msg> </div> <script> Vue.component('info',{ template:` <div> <button @click="send">click</button> </div> `, data(){ return{ msg:'I am from info component' } }, methods:{ send:function(){ this.$emit('child',this.msg) } } }) Vue.component('msg',{ template:` <div> {{this.value}} </div> `, props:['value'] }) var app = new Vue({ el:'#app', data:{ value:'' }, methods:{ accept:function(msg){ this.value = msg; } } }) </script> </body> </html>
至於說複雜傳值是經過vuex狀態管理器來實現的,這個放在後面說
web
動態組件就是經過is屬性動態的切換組件,它是爲了實現讓多個組件使用同一個掛載點,並動態切換,動態切換的組件是被移除掉了,若是把切換出去的組件保留在內存中,能夠保留它的狀態或避免從新渲染,這就須要keepalive來保存
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Vue 測試實例 - 動態組件</title> <script src="https://cdn.bootcss.com/vue/2.2.2/vue.min.js"></script> </head> <body> <div id="app"> <button @click='toShow'>點擊顯示子組件</button> <keep-alive> <component v-bind:is="which_to_show" ></component> </keep-alive> </div> <script> var vm = new Vue({ el: '#app', data: { which_to_show: "first" }, methods: { toShow: function () { //切換組件顯示 var arr = ["first", "second", "third", ""]; var index = arr.indexOf(this.which_to_show); if (index < 2) { this.which_to_show = arr[index + 1]; } else { this.which_to_show = arr[0]; } console.log(this.$children); } }, components: { first: { //第一個子組件 template: "<div>這裏是子組件1</div>" }, second: { //第二個子組件 template: "<div>這裏是子組件2,這裏是延遲後的內容:{{hello}}</div>", data: function () { return { hello: "" } }, activated: function (done) { //執行這個參數時,纔會切換組件 var self = this; var startTime = new Date().getTime(); // get the current time //兩秒後執行 while (new Date().getTime() < startTime + 2000){ self.hello='我是延遲後的內容'; } } }, third: { //第三個子組件 template: "<div>這裏是子組件3</div>" } } }); </script> </body> </html>
初始狀況下,vm.$children屬性中只有一個元素(first組件),點擊按鈕切換後,vm.$children屬性中有兩個元素,再次切換後,則有三個元素(三個子組件都保留在內存中)。以後不管如何切換,將一直保持有三個元素。
異步組件:將應用分割成小一些的代碼塊,只在須要的時候才從服務器加載一個模塊
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>異步組件</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> <async></async> </div> <script> Vue.component('async', function (resolve, reject) { setTimeout(function () { resolve({ template: '<div>I am async!</div>' }) }, 1000) }) new Vue({ el: '#app' }) </script> </body> </html>
插槽:組件插入內容和組件內部的聯通,有基本插槽,具名插槽和做用域插槽
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>插槽</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> <me-component> <h1>我是header</h1> </me-component> </div> <script > Vue.component('me-component', { template: `<div> <p>你好</p> <slot></slot> </div>` }) var vm = new Vue({ el:"#app" }) </script > </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>插槽</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> <me-component> <h2 slot="footer">插槽內容</h2> </me-component> </div> <script > Vue.component('me-component', { template: `<div> <p>模板內容</p> <slot name="footer"></slot> </div>` }) var vm = new Vue({ el:"#app" }) </body> </html>
做用域插槽:組件擦汗日內容接收到組件內部的數據
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>做用域插槽</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <me> <template v-slot:default="slotProps"> <div>{{slotProps.user.firstName}}</div> </template> </me> </div> <script> Vue.component('me',{ template:`<div> <slot :user="user">hello</slot> </div>`, data(){ return { user:{ firstName:'kk', lastNmae:'G' } } } }) var app = new Vue({ el:'#app' }) </script> </body> </html>
爲狀態發生改變的內容添加動態效果
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <style> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style> </head> <body> <div id="demo"> <button v-on:click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition> </div> <script> new Vue({ el: '#demo', data: { show: true } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <link rel="stylesheet" href="https://cdn.bootcss.com/animate.css/3.7.0/animate.min.css"> <style> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style> </head> <body> <div id="example-3"> <button @click="show = !show"> Toggle render </button> <transition name="custom-classes-transition" enter-active-class="animated tada" leave-active-class="animated bounceOutRight" > <p v-if="show">hello</p> </transition> </div> <script> new Vue({ el: '#example-3', data: { show: true } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script> </head> <body> <div id="example-4"> <button @click="show = !show"> Toggle </button> <transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:leave="leave" v-bind:css="false" > <p v-if="show"> Demo </p> </transition> </div> <script> new Vue({ el: '#example-4', data: { show: false }, methods: { beforeEnter: function (el) { el.style.opacity = 0 el.style.transformOrigin = 'left' }, enter: function (el, done) { Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 }) Velocity(el, { fontSize: '1em' }, { complete: done }) }, leave: function (el, done) { Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 }) Velocity(el, { rotateZ: '100deg' }, { loop: 2 }) Velocity(el, { rotateZ: '45deg', translateY: '30px', translateX: '30px', opacity: 0 }, { complete: done }) } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <style> .component-fade-enter-active, .component-fade-leave-active { transition: opacity .3s ease; } .component-fade-enter, .component-fade-leave-to { opacity: 0; } </style> </head> <body> <div id="transition-components-demo"> A:<input type="radio" :checked="aIsChecked" @click="handleClick" > B:<input type="radio" :checked="bIsChecked" @click="handleClick" > <transition name="component-fade" mode="out-in"> <component v-bind:is="view"></component> </transition> </div> <script> new Vue({ el: '#transition-components-demo', data: { view: 'v-a', aIsChecked:true, bIsChecked:false }, components: { 'v-a': { template: '<div>Component A</div>' }, 'v-b': { template: '<div>Component B</div>' } }, methods:{ handleClick:function(){ this.aIsChecked = !this.aIsChecked; this.bIsChecked = !this.bIsChecked; this.view = this.view === 'v-a' ? 'v-b' : 'v-a'; } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <style> .list-item { display: inline-block; margin-right: 10px; } .list-enter-active, .list-leave-active { transition: all 1s; } .list-enter, .list-leave-to{ opacity: 0; transform: translateY(30px); } </style> </head> <body> <div id="list-demo" class="demo"> <button v-on:click="add">Add</button> <button v-on:click="remove">Remove</button> <transition-group name="list" tag="p"> <span v-for="item in items" v-bind:key="item" class="list-item"> {{ item }} </span> </transition-group> </div> <script> new Vue({ el: '#list-demo', data: { items: [1,2,3,4,5,6,7,8,9], nextNum: 10 }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) }, } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script> <script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script> <style> .example-7-color-preview { display: inline-block; width: 50px; height: 50px; } </style> </head> <body> <div id="example-7"> <input v-model="colorQuery" v-on:keyup.enter="updateColor" placeholder="Enter a color" > <button v-on:click="updateColor">Update</button> <p>Preview:</p> <span v-bind:style="{ backgroundColor: tweenedCSSColor }" class="example-7-color-preview" ></span> <p>{{ tweenedCSSColor }}</p> </div> <script> var Color = net.brehaut.Color new Vue({ el: '#example-7', data: { colorQuery: '', color: { red: 0, green: 0, blue: 0, alpha: 1 }, tweenedColor: {} }, created: function () { this.tweenedColor = Object.assign({}, this.color) }, watch: { color: function () { function animate () { if (TWEEN.update()) { requestAnimationFrame(animate) } } new TWEEN.Tween(this.tweenedColor) .to(this.color, 750) .start() animate() } }, computed: { tweenedCSSColor: function () { return new Color({ red: this.tweenedColor.red, green: this.tweenedColor.green, blue: this.tweenedColor.blue, alpha: this.tweenedColor.alpha }).toCSS() } }, methods: { updateColor: function () { this.color = new Color(this.colorQuery).toRGB() this.colorQuery = '' } } }) </script> </body> </html>
//指令參數 el: 指令所綁定的元素,能夠用來直接操做 DOM 。 binding: 一個對象,包含如下屬性: name: 指令名,不包括 v- 前綴。 value: 指令的綁定值, 例如: v-my-directive="1 + 1", value 的值是 2。 oldValue: 指令綁定的前一個值,僅在 update 和 componentUpdated 鉤子中可用。不管值是否改變均可用。 expression: 綁定值的字符串形式。 例如 v-my-directive="1 + 1" , expression 的值是 "1 + 1"。 arg: 傳給指令的參數。例如 v-my-directive:foo, arg 的值是 "foo"。 modifiers: 一個包含修飾符的對象。 例如: v-my-directive.foo.bar, 修飾符對象 modifiers 的值是 { foo: true, bar: true }。 vnode: Vue 編譯生成的虛擬節點。 oldVnode: 上一個虛擬節點,僅在 update 和 componentUpdated 鉤子中可用。
//指令鉤子 bind: 只調用一次,指令第一次綁定到元素時調用,用這個鉤子函數能夠定義一個在綁定時執行一次的初始化動做。 inserted: 被綁定元素插入父節點時調用(父節點存在便可調用,沒必要存在於 document 中)。 update: 被綁定元素所在的模板更新時調用,而不論綁定值是否變化。經過比較更新先後的綁定值,能夠忽略沒必要要的模板更新(詳細的鉤子函數參數見下)。 componentUpdated: 被綁定元素所在模板完成一次更新週期時調用。 unbind: 只調用一次, 指令與元素解綁時調用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>列表過渡</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <div id="hook-arguments-example" v-demo:foo.a.b="message"></div> </div> <script> Vue.directive('demo', { bind: function (el, binding, vnode) { var s = JSON.stringify el.innerHTML = 'name: ' + s(binding.name) + '<br>' + 'value: ' + s(binding.value) + '<br>' + 'expression: ' + s(binding.expression) + '<br>' + 'argument: ' + s(binding.arg) + '<br>' + 'modifiers: ' + s(binding.modifiers) + '<br>' + 'vnode keys: ' + Object.keys(vnode).join(', ') } }) new Vue({ el: '#hook-arguments-example', data: { message: 'hello!' } }) </script> </body> </html>
混入會將公用的組件配置項與默認配置項目混合,具體看例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>混入</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script> </head> <body> <div id="app"> <mixinsComponent></mixinsComponent> </div> <script> var myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin!') } } } // 定義一個使用混入對象的組件 var Component = Vue.extend({ mixins: [myMixin] }) var mixinsComponent = new Component() var app = new Vue({ el:'#app', component:{ mixinsComponent } }) </script> </body> </html>
vue默認使用html的模板而後綁定在虛擬dom,但還提供了渲染函數實現更增強大的原生js使用能力
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>渲染函數</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> <test :test="1"></test> </div> <script> Vue.component('test',{ render:function(createElement){ return createElement('div',{ class:{ test:true }, id:{ test:true }, style:{ color:'green' }, props:{ test:"test" }, domProps: { innerHTML: 'hello world' }, on: { click: function(){ alert('click event') } } }) } }) var app = new Vue({ el:'#app' }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>插件</title> <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script> </head> <body> <div id="app"> </div> <script> var Test = {} Test.install = function(Vue,options){ Vue.globalVarible = 'hello world'; Vue.mixin({ mounted:function(){ alert(this.globalVarible) } }) }; Vue.use(Test); var app = new Vue({ el:'#app' }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/foo">Go to Foo</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/foo', component:{ template:`<div>foo</div>` } }, { path:'/', component:{ template:`<div>index</div>` } } ] }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/foo/one">Go to Foo one</router-link> <router-link to="/foo/two">Go to Foo two</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/foo/:name', component:{ template:`<div>{{$route.params.name}}</div>` } }, { path:'/', component:{ template:`<div>index</div>` } } ] }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/">index</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/', component:{ template:` <div> <div>index</div> <router-link to="bar" append>bar</router-link> <router-view></router-view> </div> ` }, children:[ { path:'bar', component:{ template:`<div>hello</div>` } } ] } ] }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/">index</router-link> <router-link to="/list">list</router-link> <router-link to="/detail">detail</router-link> <router-view></router-view> <button @click="change">導航</button> </div> <script> var router = new VueRouter({ routes:[ { path:'/', component:{ template:` <div> index </div> ` } }, { path:'/list', component:{ template:` <div> list </div> ` } }, { path:'/detail', component:{ template:` <div> detail </div> ` } } ] }) var app = new Vue({ el:'#app', router:router, methods:{ change:function(){ setTimeout(function(){ this.router.push('/list') setTimeout(function(){ this.router.replace('/detail') },2000) },2000) } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link :to={name:'index'}>index</router-link> <router-view name="info"></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/', name:'index', components:{ name:{ template:` <div> name </div> ` }, info:{ template:` <div> info </div> ` } } } ] }) var app = new Vue({ el:'#app', router:router, methods:{ change:function(){ setTimeout(function(){ this.router.push('/list') setTimeout(function(){ this.router.replace('/detail') },2000) },2000) } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link :to={name:'index'}>index</router-link> <router-link to="/list">list</router-link> <router-link to="/compute">detail</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/', redirect:'/list', component:{ template:` <div> index </div> ` } }, { path:'/list', component:{ template:` <div> list </div> ` } }, { path:'/detail', component:{ template:` <div> detail </div> ` }, alias:'/compute' } ] }) var app = new Vue({ el:'#app', router:router, methods:{ change:function(){ setTimeout(function(){ this.router.push('/list') setTimeout(function(){ this.router.replace('/detail') },2000) },2000) } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/foo/one">Go to Foo one</router-link> <router-link to="/foo/two">Go to Foo two</router-link> <router-link to="/s/one">Go to Foo one</router-link> <router-link to="/s/two">Go to Foo two</router-link> <router-view></router-view> </div> <script> var pageId = { template:`<div>{{name}}</div>`, props:['name'] } var router = new VueRouter({ routes:[ { path:'/foo/:name', component:pageId, props:true }, { path:'/s/:name', component:pageId, props:true }, { path:'/', component:{ template:`<div>index</div>` } } ] }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/">index</router-link> <router-link to="/user">user</router-link> <router-link to="/login">login</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/', component:{ template:`<div>index</div>` } }, { path:'/user', component:{ template:`<div>user</div>` } }, { path:'/login', component:{ template:`<div>login</div>` } } ] }) router.beforeEach(function(to,from,next){ var login = false; if(!login && to.path === '/user'){ next('/login') }else{ next() } }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/">index</router-link> <router-link to="/user">user</router-link> <router-link to="/login">login</router-link> <router-view></router-view> </div> <script> var router = new VueRouter({ routes:[ { path:'/', component:{ template:`<div>index</div>` } }, { path:'/user', component:{ template:`<div>user</div>` }, meta:{ flag:true } }, { path:'/login', component:{ template:`<div>login</div>` } } ] }) router.beforeEach(function(to,from,next){ var login = true; if(!login && to.matched.some(function(item) { return item.meta.flag })){ next('/login') }else{ next() } }) var app = new Vue({ el:'#app', router:router }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>路由</title> <link href="https://cdn.bootcss.com/animate.css/3.7.0/animate.css" rel="stylesheet"> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> </head> <body> <div id="app"> <router-link to="/">index</router-link> <router-link to="/list">list</router-link> <router-link to="/detail">detail</router-link> <transition :enter-active-class="effect"> <router-view></router-view> </transition> </div> <script> var router = new VueRouter({ routes:[ { path:'/', component:{ template:` <div> index </div> ` } }, { path:'/list', component:{ template:` <div> list </div> ` } }, { path:'/detail', component:{ template:` <div> detail </div> ` }, } ] }) var app = new Vue({ el:'#app', router:router, data:{ effect:'' }, watch:{ '$route'(to,from){ var effectMap = { "/":"shake", "/list":"jello", "/detail":"bounce" }; this.effect = ['animated',effectMap[to.path]].join(' ') } } }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>狀態管理</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.js"></script> </head> <body> <div id="app"> {{count}} </div> <script> var store = new Vuex.Store({ state:{ count:0 }, mutations:{ increment(state){ state.count++ } } }) store.commit('increment') var app = new Vue({ el:"#app", store, computed:Vuex.mapState(['count']) }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>狀態管理</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.js"></script> </head> <body> <div id="app"> {{add(2)}} </div> <script> var store = new Vuex.Store({ state:{ count:0 }, getters:{ add:state => n => { return state.count + n; } } }) var app = new Vue({ el:"#app", store, computed:Vuex.mapGetters(['add']) }) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>狀態管理</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.js"></script> </head> <body> <div id="app"> </div> <script> var ADD = 'ADD'; var store = new Vuex.Store({ state:{ count:0 }, mutations:{ [ADD](state,payload){ state.count+=payload.n } } }) store.commit({ type:'ADD', n:10 }) console.log(store.state.count) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>狀態管理</title> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.js"></script> </head> <body> <div id="app"> </div> <script> var ADD = 'ADD'; var store = new Vuex.Store({ state:{ count:0 }, mutations:{ increment(state){ state.count++ } }, actions:{ [ADD](state,payload){ setTimeout(()=>{ commit('increment') },3000) } } }) store.dispatch('ADD') </script> </body> </html>
實現純瀏覽器端渲染的關鍵是render: h => h(App),render會將app轉化成html的節點插入到頁面
目錄結構
- node_modules - components - Bar.vue - Foo.vue - App.vue - app.js - index.html - webpack.config.js - package.json - yarn.lock - postcss.config.js - .babelrc - .gitignore
app.js
import Vue from 'vue'; import App from './App.vue'; let app = new Vue({ el: '#app', render: h => h(App) });
app.vue
<template> <div> <Foo></Foo> <Bar></Bar> </div> </template> <script> import Foo from './components/Foo.vue'; import Bar from './components/Bar.vue'; export default { components: { Foo, Bar } } </script>
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>純瀏覽器渲染</title> </head> <body> <div id="app"></div> </body> </html>
components/Foo.vue
<template> <div class="foo"> <h1>Foo Component</h1> </div> </template> <style> .foo { background: yellowgreen; } </style>
components/Bar.vue
<template> <div class="bar"> <h1>Bar Component</h1> </div> </template> <style> .bar { background: bisque; } </style>
webpack.config.js
const path = require('path'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { mode: 'development', entry: './app.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader', 'postcss-loader'] // 若是須要單獨抽出CSS文件,用下面這個配置 // use: ExtractTextPlugin.extract({ // fallback: 'vue-style-loader', // use: [ // 'css-loader', // 'postcss-loader' // ] // }) }, { test: /\.(jpg|jpeg|png|gif|svg)$/, use: { loader: 'url-loader', options: { limit: 10000 // 10Kb } } }, { test: /\.vue$/, use: 'vue-loader' } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: './index.html' }), // 若是須要單獨抽出CSS文件,用下面這個配置 // new ExtractTextPlugin("styles.css") ] };
postcss.config.js
module.exports = { plugins: [ require('autoprefixer') ] };
.babelrc
{ "presets": [ "@babel/preset-env" ], "plugins": [ // 讓其支持動態路由的寫法 const Foo = () => import('../components/Foo.vue') "dynamic-import-webpack" ] }
package.json
{ "name": "01", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "start": "yarn run dev", "dev": "webpack-dev-server", "build": "webpack" }, "dependencies": { "vue": "^2.5.17" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", "babel-plugin-dynamic-import-webpack": "^1.1.0", "autoprefixer": "^9.1.5", "babel-loader": "^8.0.4", "css-loader": "^1.0.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^2.0.0", "html-webpack-plugin": "^3.2.0", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "url-loader": "^1.1.1", "vue-loader": "^15.4.2", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.5.17", "webpack": "^4.20.2", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.9" } }
啓動
yarn start
構建
yarn run build
效果圖:
讓一份代碼既能夠在服務端運行,也能夠在客戶端運行,若是說在SSR的過程當中出現問題,還能夠回滾到純瀏覽器渲染,保證用戶正常看到頁面,順着這個思路,確定就會有兩個webpack的入口文件,一個用於瀏覽器端渲染weboack.client.config.js,一個用於服務端渲染webpack.server.config.js,將它們的公有部分抽出來做爲webpack.base.cofig.js,後續經過webpack-merge進行合併。同時,也要有一個server來提供http服務,我這裏用的是koa
實現這些的關鍵是require('vue-server-renderer').createBundleRenderer(server.bundle.js,{ template:"index.ssr.html"}).renderToString((err, html) => { })
目錄結構
- node_modules - config // 新增 - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js - src - components - Bar.vue - Foo.vue - App.vue - app.js - entry-client.js // 新增 - entry-server.js // 新增 - index.html - index.ssr.html // 新增 - package.json - yarn.lock - postcss.config.js - .babelrc - .gitignore
對app.js作修改,將其包裝爲一個工廠函數,每次調用都會生成一個全新的根組件
app.js
import Vue from 'vue'; import App from './App.vue'; export function createApp() { const app = new Vue({ render: h => h(App) }); return { app }; }
在瀏覽器端,咱們直接新建一個根組件,而後將其掛載就能夠了
entry-client.js
import { createApp } from './app.js'; const { app } = createApp(); app.$mount('#app');
在服務器端,咱們就要返回一個函數,該函數的做用是接收一個context參數,同時每次都返回一個新的根組件。這個context在這裏咱們還不會用到,後續的步驟會用到它
entry-server.js
import { createApp } from './app.js'; export default context => { const { app } = createApp(); return app; }
而後再來看一下index.ssr.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>服務端渲染</title> </head> <body> <!--vue-ssr-outlet--> <script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script> </body> </html>
<!--vue-ssr-outlet-->的做用是做爲一個佔位符,後續經過vue-server-renderer插件,將服務器解析出的組件html字符串插入到這裏。<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>是爲了將webpack經過webpack.client.config.js打包出的文件放到這裏
由於服務端吐出來的就是一個html字符串,後續的Vue相關的響應式、事件響應等等,都須要瀏覽器端來接管,因此就須要將爲瀏覽器端渲染打包的文件在這裏引入,用官方的詞來講,叫客戶端激活(client-side hydration)
所謂客戶端激活,指的是 Vue 在瀏覽器端接管由服務端發送的靜態 HTML,使其變爲由 Vue 管理的動態 DOM 的過程
在 entry-client.js 中,咱們用下面這行掛載(mount)應用程序
// 這裏假定 App.vue template 根元素的 `id="app"` app.$mount('#app')
因爲服務器已經渲染好了 HTML,咱們顯然無需將其丟棄再從新建立全部的 DOM 元素。相反,咱們須要"激活"這些靜態的 HTML,而後使他們成爲動態的(可以響應後續的數據變化)。
若是你檢查服務器渲染的輸出結果,你會注意到應用程序的根元素上添加了一個特殊的屬性:
<div id="app" data-server-rendered="true">
Vue在瀏覽器端就依靠這個屬性將服務器吐出來的html進行激活,咱們一會本身構建一下就能夠看到了。接下來咱們看一下webpack相關的配置:
webpack.base.config.js
const path = require('path'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); module.exports = { mode: 'development', resolve: { extensions: ['.js', '.vue'] }, output: { path: path.resolve(__dirname, '../dist'), filename: '[name].bundle.js' }, module: { rules: [ { test: /\.vue$/, use: 'vue-loader' }, { test: /\.js$/, use: 'babel-loader' }, { test: /\.css$/, use: ['vue-style-loader', 'css-loader', 'postcss-loader'] }, { test: /\.(jpg|jpeg|png|gif|svg)$/, use: { loader: 'url-loader', options: { limit: 10000 // 10Kb } } } ] }, plugins: [ new VueLoaderPlugin() ] };
webpack.client.config.js
const path = require('path'); const merge = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const base = require('./webpack.base.config'); module.exports = merge(base, { entry: { client: path.resolve(__dirname, '../src/entry-client.js') }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.html'), filename: 'index.html' }) ] });
注意,這裏的入口文件變成了entry-client.js,將其打包出的client.bundle.js插入到index.html中
webpack.server.config.js
const path = require('path'); const merge = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const base = require('./webpack.base.config'); module.exports = merge(base, { target: 'node', entry: { server: path.resolve(__dirname, '../src/entry-server.js') }, output: { libraryTarget: 'commonjs2' }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.ssr.html'), filename: 'index.ssr.html', files: { js: 'client.bundle.js' }, excludeChunks: ['server'] }) ] });
這裏有幾個點須要注意一下:
入口文件是 entry-server.js,由於是打包服務器端依賴的代碼,因此target要設爲node,同時,output的libraryTarget要設爲commonjs2
這裏關於HtmlWebpackPlugin配置的意思是,不要在index.ssr.html中引入打包出的server.bundle.js,要引爲瀏覽器打包的client.bundle.js,緣由前面說過了,是爲了讓Vue能夠將服務器吐出來的html進行激活,從而接管後續響應。
那麼打包出的server.bundle.js在哪用呢?接着往下看就知道啦
package.json
{ "name": "01", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "start": "yarn run dev", "dev": "webpack-dev-server", "build:client": "webpack --config config/webpack.client.config.js", "build:server": "webpack --config config/webpack.server.config.js" }, "dependencies": { "koa": "^2.5.3", "koa-router": "^7.4.0", "koa-static": "^5.0.0", "vue": "^2.5.17", "vue-server-renderer": "^2.5.17" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", "autoprefixer": "^9.1.5", "babel-loader": "^8.0.4", "css-loader": "^1.0.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^2.0.0", "html-webpack-plugin": "^3.2.0", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "style-loader": "^0.23.0", "url-loader": "^1.1.1", "vue-loader": "^15.4.2", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.5.17", "webpack": "^4.20.2", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.1.9", "webpack-merge": "^4.1.4" } }
接下來咱們看server端關於http服務的代碼:
server/server.js
const Koa = require('koa'); const Router = require('koa-router'); const serve = require('koa-static'); const path = require('path'); const fs = require('fs'); const backendApp = new Koa(); const frontendApp = new Koa(); const backendRouter = new Router(); const frontendRouter = new Router(); const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8'); const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8') }); // 後端Server backendRouter.get('/index', (ctx, next) => { // 這裏用 renderToString 的 promise 返回的 html 有問題,沒有樣式 renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = '服務器內部錯誤'; } else { console.log(html); ctx.status = 200; ctx.body = html; } }); }); backendApp.use(serve(path.resolve(__dirname, '../dist'))); backendApp .use(backendRouter.routes()) .use(backendRouter.allowedMethods()); backendApp.listen(3000, () => { console.log('服務器端渲染地址: http://localhost:3000'); }); // 前端Server frontendRouter.get('/index', (ctx, next) => { let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.html'), 'utf-8'); ctx.type = 'html'; ctx.status = 200; ctx.body = html; }); frontendApp.use(serve(path.resolve(__dirname, '../dist'))); frontendApp .use(frontendRouter.routes()) .use(frontendRouter.allowedMethods()); frontendApp.listen(3001, () => { console.log('瀏覽器端渲染地址: http://localhost:3001'); });
這裏對兩個端口進行監聽,3000端口是服務端渲染,3001端口是直接輸出index.html,而後會在瀏覽器端走Vue的那一套,主要是爲了和服務端渲染作對比使用。這裏的關鍵代碼是如何在服務端去輸出html字符串
const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8'); const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8') });
能夠看到,server.bundle.js在這裏被使用啦,由於它的入口是一個函數,接收context做爲參數(非必傳),輸出一個根組件app
這裏咱們用到了vue-server-renderer插件,它有兩個方法能夠作渲染,一個是createRenderer,另外一個是createBundleRenderer
const { createRenderer } = require('vue-server-renderer') const renderer = createRenderer({ /* 選項 */ })
const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer(serverBundle, { /* 選項 */ })
createRenderer沒法接收爲服務端打包出的server.bundle.js文件,因此這裏只能用createBundleRenderer
serverBundle 參數能夠是如下之一:
這裏咱們引入的是.js文件,後續會介紹如何使用.json文件以及有什麼好處。
renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = '服務器內部錯誤'; } else { console.log(html); ctx.status = 200; ctx.body = html; } });
使用createRenderer和createBundleRenderer返回的renderer函數包含兩個方法renderToString和renderToStream,咱們這裏用的是renderToString成功後直接返回一個完整的字符串,renderToStream返回的是一個Node流。
renderToString支持Promise,可是我在使用Prmoise形式的時候樣式會渲染不出來,暫時還不知道緣由
配置基本就完成了,來看一下如何運行。
yarn run build:client // 打包瀏覽器端須要bundle yarn run build:server // 打包SSR須要bundle yarn start // 其實就是 node server/server.js,提供http服務
若是SSR須要初始化一些異步數據,那麼流程就會變得複雜一些,咱們先提出幾個問題:
服務器端渲染和瀏覽器端渲染組件通過的生命週期是有區別的,在服務器端,只會經歷beforeCreate和created兩個生命週期。由於SSR服務器直接吐出html字符串就行了,不會渲染DOM結構,因此不存在beforeMount和mounted的,也不會對其進行更新,因此也就不存在beforeUpdate和updated等
咱們先來想一下,在純瀏覽器渲染的Vue項目中,咱們是怎麼獲取異步數據並渲染到組件中的?通常是在created或者mounted生命週期裏發起異步請求,而後在成功回調裏執行this.data = xxx,Vue監聽到數據發生改變,走後面的Dom Diff,打patch,作DOM更新
那麼服務端渲染可不能夠也這麼作呢?答案是不行的。
那應該怎麼作呢?
正常狀況下,經過這幾個步驟,服務端吐出來的html字符串相應組件的數據都是最新的,因此第4步並不會引發DOM更新,但若是出了某些問題,吐出來的html字符串沒有相應數據,Vue也能夠在瀏覽器端經過
Vuex注入數據,進行DOM更新。`更新後的目錄
- node_modules - config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js - src - components - Bar.vue - Foo.vue - store // 新增 store.js - App.vue - app.js - entry-client.js - entry-server.js - index.html - index.ssr.html - package.json - yarn.lock - postcss.config.js - .babelrc - .gitignore
store/store.js
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const fetchBar = function() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('bar 組件返回 ajax 數據'); }, 1000); }); }; function createStore() { const store = new Vuex.Store({ state: { bar: '' }, mutations: { 'SET_BAR'(state, data) { state.bar = data; } }, actions: { fetchBar({ commit }) { return fetchBar().then((data) => { commit('SET_BAR', data); }).catch((err) => { console.error(err); }) } } }); if (typeof window !== 'undefined' && window.__INITIAL_STATE__) { console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__); store.replaceState(window.__INITIAL_STATE__); } return store; } export default createStore; typeof window
這裏fetchBar能夠當作是一個異步請求,這裏用setTimeout模擬。在成功回調中commit相應的mutation進行狀態修改。
if (typeof window !== 'undefined' && window.__INITIAL_STATE__) { console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__); store.replaceState(window.__INITIAL_STATE__); }
由於store.js一樣也會被打包到服務器運行的server.bundle.js中,因此運行環境不必定是瀏覽器,這裏須要對window作判斷,防止報錯,同時若是有window.__INITIAL_STATE__屬性,說明服務器已經把全部初始化須要的異步數據都獲取完成了,要對store中的狀態作一個替換,保證統一
components/Bar.vue
<template> <div class="bar"> <h1 @click="onHandleClick">Bar Component</h1> <h2>異步Ajax數據:</h2> <span>{{ msg }}</span> </div> </template> <script> const fetchInitialData = ({ store }) => { store.dispatch('fetchBar'); }; export default { asyncData: fetchInitialData, methods: { onHandleClick() { alert('bar'); } }, mounted() { // 由於服務端渲染只有 beforeCreate 和 created 兩個生命週期,不會走這裏 // 因此把調用 Ajax 初始化數據也寫在這裏,是爲了供單獨瀏覽器渲染使用 let store = this.$store; fetchInitialData({ store }); }, computed: { msg() { return this.$store.state.bar; } } } </script> <style> .bar { background: bisque; } </style>
這裏在Bar組件的默認導出對象中增長了一個方法asyncData,在該方法中會dispatch相應的action,進行異步數據獲取。須要注意的是,我在mounted中也寫了獲取數據的代碼,這是爲何呢? 由於想要作到同構,代碼單獨在瀏覽器端運行,也應該是沒有問題的,又因爲服務器沒有mounted生命週期,因此我寫在這裏就能夠解決單獨在瀏覽器環境使用也能夠發起一樣的異步請求去初始化數據。
components/Foo.vue
<template> <div class="foo"> <h1 @click="onHandleClick">Foo Component</h1> </div> </template> <script> export default { methods: { onHandleClick() { alert('foo'); } }, } </script> <style> .foo { background: yellowgreen; } </style>
這裏我對兩個組件都添加了一個點擊事件,爲的是證實在服務器吐出首頁html後,後續的步驟都會被瀏覽器端的Vue接管,能夠正常執行後面的操做
app.js
import Vue from 'vue'; import createStore from './store/store.js'; import App from './App.vue'; export function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return { app, store, App }; }
在創建根組件的時候,要把Vuex的store傳進去,同時要返回,後續會用到。最後來看一下entry-server.js,關鍵步驟在這裏:
entry-server.js
import { createApp } from './app.js'; export default context => { return new Promise((resolve, reject) => { const { app, store, App } = createApp(); let components = App.components; let asyncDataPromiseFns = []; Object.values(components).forEach(component => { if (component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); } }); Promise.all(asyncDataPromiseFns).then((result) => { // 當使用 template 時,context.state 將做爲 window.__INITIAL_STATE__ 狀態,自動嵌入到最終的 HTML 中 context.state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); }); }
咱們經過導出的App拿到了全部它下面的components,而後遍歷,找出哪些component有asyncData方法,有的話調用並傳入store,該方法會返回一個Promise,咱們使用Promise.all等全部的異步方法都成功返回,才resolve(app)。context.state = store.state做用是,當使用createBundleRenderer時,若是設置了template選項,那麼會把context.state的值做爲window.__INITIAL_STATE__自動插入到模板html中。
運行:
yarn run build:client yarn run build:server yarn start
能夠看到window.__INITIAL_STATE__被自動插入了。咱們來對比一下SSR到底對加載性能有什麼影響吧。服務端渲染時performance截圖:
純瀏覽器端渲染時performance截圖:
一樣都是在fast 3G網絡模式下,純瀏覽器端渲染首屏加載花費時間2.9s,由於client.js加載就花費了2.27s,由於沒有client.js就沒有Vue,也就沒有後面的東西了。服務端渲染首屏時間花費0.8s,雖然client.js加載扔花費2.27s,可是首屏已經不須要它了,它是爲了讓Vue在瀏覽器端進行後續接管。從這咱們能夠真正的看到,服務端渲染對於提高首屏的響應速度是頗有做用的。固然有的同窗可能會問,在服務端渲染獲取初始ajax數據時,咱們還延時了1s,在這個時間用戶也是看不到頁面的。沒錯,接口的時間咱們沒法避免,就算是純瀏覽器渲染,首頁該調接口仍是得調,若是接口響應慢,那麼純瀏覽器渲染看到完整頁面的時間會更慢。
前面咱們建立服務端renderer的方法是:
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') });
serverBundle咱們用的是打包出的server.bundle.js文件。這樣作的話,在每次編輯過應用程序源代碼以後,都必須中止並重啓服務。這在開發過程當中會影響開發效率。此外,Node.js 自己不支持 source map
vue-server-renderer 提供一個名爲 createBundleRenderer 的 API,用於處理此問題,經過使用 webpack 的自定義插件,server bundle 將生成爲可傳遞到 bundle renderer 的特殊 JSON 文件。所建立的 bundle renderer,用法和普通 renderer 相同,可是 bundle renderer 提供如下優勢:
那麼咱們來修改webpack配置:
webpack.client.config.js
const path = require('path'); const merge = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const VueSSRClientPlugin = require('vue-server-renderer/client-plugin'); const base = require('./webpack.base.config'); module.exports = merge(base, { entry: { client: path.resolve(__dirname, '../src/entry-client.js') }, plugins: [ new VueSSRClientPlugin(), // 新增 new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.html'), filename: 'index.html' }) ] });
webpack.server.config.js
const path = require('path'); const merge = require('webpack-merge'); const nodeExternals = require('webpack-node-externals'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const VueSSRServerPlugin = require('vue-server-renderer/server-plugin'); const base = require('./webpack.base.config'); module.exports = merge(base, { target: 'node', // 對 bundle renderer 提供 source map 支持 devtool: '#source-map', entry: { server: path.resolve(__dirname, '../src/entry-server.js') }, externals: [nodeExternals()], // 新增 output: { libraryTarget: 'commonjs2' }, plugins: [ new VueSSRServerPlugin(), // 這個要放到第一個寫,不然 CopyWebpackPlugin 不起做用,緣由還沒查清楚 new HtmlWebpackPlugin({ template: path.resolve(__dirname, '../src/index.ssr.html'), filename: 'index.ssr.html', files: { js: 'client.bundle.js' }, excludeChunks: ['server'] }) ] });
由於是服務端引用模塊,因此不須要打包node_modules中的依賴,直接在代碼中require引用就好,因此配置externals: [nodeExternals()]
兩個配置文件會分別生成vue-ssr-client-manifest.json和vue-ssr-server-bundle.json。做爲createBundleRenderer的參數
來看server.js:
const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json')); const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json')); const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8'); const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template: template, clientManifest: clientManifest });
效果和第三步就是同樣的啦
在src下新增router目錄:
import Vue from 'vue'; import Router from 'vue-router'; import Bar from '../components/Bar.vue'; Vue.use(Router); function createRouter() { const routes = [ { path: '/bar', component: Bar }, { path: '/foo', component: () => import('../components/Foo.vue') // 異步路由 } ]; const router = new Router({ mode: 'history', routes }); return router; } export default createRouter;
這裏咱們把Foo組件做爲一個異步組件引入,作成按需加載。在app.js中引入router,並導出:
app.js
import Vue from 'vue'; import createStore from './store/store.js'; import createRouter from './router'; import App from './App.vue'; export function createApp() { const store = createStore(); const router = createRouter(); const app = new Vue({ router, store, render: h => h(App) }); return { app, store, router, App }; }
修改App.vue引入路由組件:
App.vue
<template> <div id="app"> <router-link to="/bar">Goto Bar</router-link> <router-link to="/foo">Goto Foo</router-link> <router-view></router-view> </div> </template> <script> export default { beforeCreate() { console.log('App.vue beforeCreate'); }, created() { console.log('App.vue created'); }, beforeMount() { console.log('App.vue beforeMount'); }, mounted() { console.log('App.vue mounted'); } } </script>
最重要的修改在entry-server.js中,
import { createApp } from './app.js'; export default context => { return new Promise((resolve, reject) => { const { app, store, router, App } = createApp(); router.push(context.url); router.onReady(() => { const matchedComponents = router.getMatchedComponents(); console.log(context.url) console.log(matchedComponents) if (!matchedComponents.length) { return reject({ code: 404 }); } Promise.all(matchedComponents.map(component => { if (component.asyncData) { return component.asyncData({ store }); } })).then(() => { // 當使用 template 時,context.state 將做爲 window.__INITIAL_STATE__ 狀態,自動嵌入到最終的 HTML 中 context.state = store.state; // 返回根組件 resolve(app); }); }, reject); }); }
這裏前面提到的context就起了大做用,它將用戶訪問的url地址傳進來,供vue-router使用。由於有異步組件,因此在router.onReady的成功回調中,去找該url路由所匹配到的組件,獲取異步數據那一套還和前面的同樣。因而,咱們就完成了一個基本完整的基於Vue + VueRouter + VuexSSR配置
訪問http://localhost:3000/bar:
參考連接:
帶你五步學會Vue SSR :https://segmentfault.com/a/11...