前面兩篇文章總結了 Vue 開發的大部分技巧和內容,最後一篇文章來對它進行一個收尾css
這篇文章咱們來談談一些 Vue 理解和實踐要求高一點的問題html
首先是生命週期這一塊內容,隨着實踐越多它的意義越大,理解也越深入前端
mixin 功能強大,對代碼複用組織都有很高的要求,算是 Vue 後期發力的高級技巧vue
服務端渲染多是學習 Vue 最後一塊陣地了,對於 SPA 框架的一個里程碑node
最後,總結一下我在使用 Vue 中使用的技巧和經驗webpack
常規操做,先點贊後觀看哦!你的點贊是我創做的動力之一!git
我將從 16 個方面來論述 Vue 開發過程當中的一些技巧和原理。若是你還未觀看上節文章,能夠移步至github
Vue 生命週期大概就是:一個從 Vue 實例的建立到組件銷燬的一個的過程。web
具體狀況下,咱們分爲幾個核心的階段,而且每一個階段都有一套鉤子函數來執行咱們須要的代碼。面試
咱們整理分類一下這些生命週期鉤子,爲了記憶方便分爲 4 大核心階段:
方便讀者記憶,這裏儘可能使用圖示:
能夠看到每個階段中的鉤子命名都很好記憶,階段開始前使用
beforeXxx
,階段後結束後使用xxxed
除這 8
個核心鉤子,另外還有 3
個新增功能型鉤子,目前總共是 11
個鉤子 順帶提一下這 3
個鉤子的功能
activated
與 deactivated
,這兩個鉤子也是一對的,分別表示被 keep-alive
緩存的組件激活和停用時調用。errorCaptured
,對組件中出現對異常錯誤進行處理,使用較少。咱們看看官方的圖解,在 Vue 教程實例這節 官方教程直接跳轉
官方圖解並無詳細解釋這張圖。我猜一方面緣由是這個圖裏面涉及的細節都是在 vue 源碼裏面體現,要真正解釋起來也沒那麼簡單。另外一方面是但願咱們多在實踐中去理解裏面的意義。
對於上面那張圖的理解,咱們須要對 Vue 源碼進行梳理,才能真正的理解。大概根據現有的源碼,我梳理了一下大體的流程:
咱們能夠清楚的看到,從 Vue 實例建立、組件掛載、渲染的一些過程當中,有着明顯的週期節點。
結合上面源碼的流程和相關實踐,簡化每個階段作了哪些時期,每個鉤子裏面是組件處於什麼狀態。
下面咱們提出一些問題:
<template>
<div>
<div class="message">
{{message}}
</div>
</div>
</template>
<script>
export default {
data() {
return {
message: '1'
}
},
methods: {
printComponentInfo(lifeName) {
console.log(lifeName)
console.log('el', this.$el)
console.log('data', this.$data)
console.log('watch', this.$watch)
}
}
}
</script>
複製代碼
// ...
beforeCreate() {
this.printComponentInfo('beforeCreate')
},
created() {
this.printComponentInfo('created')
},
beforeMount() {
this.printComponentInfo('beforeMount')
},
mounted() {
this.printComponentInfo('mounted')
},
beforeUpdate() {
this.printComponentInfo('beforeUpdate')
},
updated() {
this.printComponentInfo('updated')
},
beforeDestroy() {
this.printComponentInfo('beforeDestroy')
},
destroyed() {
this.printComponentInfo('destroyed')
},
// ...
複製代碼
beforeCreate
中methods
中方法直接報錯沒法訪問,直接訪問 el
和 data
後
發現只能訪問到
watch
, el
和 data
均不能訪問到
created
時期 el
沒法訪問到,可是能夠訪問到 data
了
beforeMount
中能夠訪問 data
可是仍然訪問不到 el
mounted
中能夠訪問到 el
了
首次加載頁面,更新階段和銷燬階段到鉤子都未觸發
咱們增長一行代碼
this.message = this.message + 1
複製代碼
若是增長在 created
階段,發現 update
鉤子仍然未觸發,可是 el
和 data
的值都變成了 2
若是增長在 mounted
階段,發現 update
鉤子此時觸發了
怎樣觸發銷燬的鉤子呢? 大概有這幾種方法
$destory
v-if
與 v-for
指令,(v-show
不行)mounted
鉤子裏面增長一行代碼手動銷燬當前組件,或者跳轉路由this.$destory('lifecycle')
複製代碼
發現beforeDestory
和 destoryed
都觸發了,並且el
、data
都同樣仍是能夠訪問到
beforeCreate
沒法訪問到 this
中的 data
、method
// 錯誤實例
beforeCreate() {
// 容許
console.log('ok')
// 不容許
this.print() // 報錯找不到
this.message = 1 // 報錯找不到
}
複製代碼
created
能夠訪問 this,但沒法訪問 dom,dom 未掛載
created() {
// 容許並推薦
this.$http.get(xxx).then(res => {
this.data = res.data
})
// 不容許
this.$el
this.$ref.demo
const a = document.getElementById('demo')
}
複製代碼
mounted
已經掛載 dom
,能夠訪問 this
mounted() {
// 容許
this.$el
this.$ref.demo
let a = document.getElementById('')
}
複製代碼
生命週期相關demo 代碼見github-lifecycle-demo
當組件使用混入對象時,全部混入對象的選項將被「混合」進入該組件自己的選項 大體原理就是將外來的組件、方法以某種方式進行合併。合併的規則有點像繼承和擴展。
當組件和混入對象含有同名選項時,這些選項將以恰當的方式進行「合併」
咱們看一下一個組件裏面有哪些東西是能夠合併的
// mixins/demo
export default {
data() {
return {}
},
mounted() {},
methods: {},
computed: {},
components: {},
directives: {}
}
複製代碼
data
、methods
、computed
、directives
、components
生命週期鉤子
沒錯這些均可以混入
import demoMixin form '@/mixins/demo'
export default {
mixins: [demoMixin]
}
複製代碼
這樣看來,不少頁面重複的代碼咱們均可以直接抽取出來
或者是封裝成一個公共的 mixin
好比咱們作 H5 頁面,裏面不少短信驗證的邏輯固有邏輯,可是須要訪問到 this
。使用工具函數確定不行。
這時候就能夠考慮使用 mixin,封裝成一個具備響應式的模塊。供須要的地方進行引入。
首先是優先級的問題,當重名選項時選擇哪個爲最後的結果
默認規則我這裏分爲 3
類
data
混入: 以當前組件值爲最後的值methods
、computed
、directives
、components
這種健值對形式,同名key
,通通以當前組件爲準固然若是想改變規則,也能夠經過配置來改變規則
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合併後的值
}
複製代碼
咱們知道 Vue 最能複用代碼的就是組件。通常狀況,咱們經過 props
來控制組件的,將原有組件封裝成 HOC 高階組件。而控制 props
的生成不同的功能的代碼仍是寫在基礎組件裏。
<template lang="pug">
div.dib
van-button.btn(
@click="$emit('click')"
:class="getClass" v-bind="$attrs"
:style="{'width': size === 'large' ? '345px': '', 'backgroundColor': getBgColor, borderColor: getBgColor, color: getBgColor}")
slot
</template>
<script>
import { Button } from 'vant'
import Vue from 'vue'
import { getColor } from '@/utils'
Vue.use(Button)
export default {
name: 'app-button',
props: {
type: {
type: String,
default: 'primary'
},
theme: {
type: String,
default: 'blue'
},
size: {
type: String,
default: ''
}
}
}
複製代碼
以這個組件爲例,咱們仍是經過公共組件內部的邏輯,來改變組件的行爲。
可是,使用 mixin 提供了另外一個思路。咱們寫好公共的 mixin,每個須要使用 mixin 的地方。咱們進行擴展合併,不一樣與公共 mixin 的選項咱們在當前組件中進行自定義,也就是擴展。咱們新的邏輯是寫在當前組件裏面的,而非公共 mixins 裏。
畫個圖理解一下:
最後總結一下 mixin 的優勢
this
, 能夠操做響應式代碼第一,千萬不能濫用全局 mixins 由於會影響全部多子組件
第二,因爲 mixins 的合併策略固有影響,可能在一些極端狀況達不到你想要的效果。
好比:我已經存在一個 mixins
,個人頁面裏面也有一些方法,我要引入mixins
就要作不少改動,來保證個人代碼按個人須要運行。
頁面 data
有一個 message
值,mixins
裏面一樣有一個。
按照默認規則,mixins
裏面的 message
會被頁面裏面 message
覆蓋。可是這兩個 message
可能表明的意義不同,都須要存在。那麼我就須要改掉其中的一個,若是業務太深的話,可能這個 message
沒那麼好改。
有時候須要考慮這些問題,致使使用 mixins
都會增長一些開發負擔。固然也是這些問題可使用規範來規避。
SSR 是 Serve Side Render 的縮寫,翻譯過來就是咱們常說的服務端渲染
可是它還存在如下問題
activated
和 deactivated
等等)總得來講,SSR 是必要的但不是充分的,SPA 的 SEO 如今沒有更好的方案,有這方面強烈需求的網站來講,SSR 確實頗有必要
經過上面圖咱們能夠得大體幾點內容
分別嘗試用這 3 種方式搭建 SSR
這裏主要加深理解,vue-cli3+ 實現基本 SSR
分爲 2
個入口,將 main.js
定爲通用入口, 並額外增長entry-client.js
和 entry-serve.js
兩個
1.改造主要入口,建立工廠函數
// main.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from "./router"
// app、router
export function createApp () {
const router = createRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
複製代碼
2.客戶端入口
// client.js
import { createApp } from './main'
// 客戶端特定引導邏輯
const { app } = createApp()
app.$mount('#app')
複製代碼
3.服務端入口
// serve.js
import { createApp } from "./main";
export default context => {
// 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 設置服務器端 router 的位置
router.push(context.url);
// 等到 router 將可能的異步組件和鉤子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,執行 reject 函數
if (!matchedComponents.length) {
return reject({
code: 404
});
}
// Promise 應該 resolve 應用程序實例,以便它能夠渲染
resolve(app);
}, reject);
});
};
複製代碼
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("webpack-merge");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
configureWebpack: () => ({
entry: `./src/entry-${target}.js`,
devtool: 'source-map',
target: TARGET_NODE ? "node" : "web",
node: TARGET_NODE ? undefined : false,
output: {
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
externals: TARGET_NODE
? nodeExternals({
whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
//...
};
複製代碼
export function createRouter(){
return new Router({
mode: 'history',
routes: [
//...
]
})
}
複製代碼
這一步主要是讓 node 服務端響應 HTML 給瀏覽器訪問
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
複製代碼
簡單幾步體驗下 nuxt
簡單看了一下源碼,nuxt 把咱們以前提到的重要的改造,所有封裝到
.nuxt
文件夾裏面了
跑一下 dev 發現有兩個端,一個 clinet 端,一個 server 端
最後查看一下效果,整個過程挺絲滑的。目錄結構也比較符合個人風格,新項目須要 SSR 會考慮使用 nuxt
解決 SEO 問題是否是隻有 SSR 呢?其實預渲染也能作到,首先
prerender-spa-plugin
yarn prerender-spa-plugin
複製代碼
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
module.exports = {
plugins: [
//...
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
outputDir: path.join(__dirname, 'prerendered'),
indexPath: path.join(__dirname, 'dist', 'index.html'),
routes: [ '/', '/about', '/some/deep/nested/route' ],
postProcess (renderedRoute) {
renderedRoute.route = renderedRoute.originalPath
renderedRoute.html = renderedRoute.html.split(/>[\s]+</gmi).join('><')
if (renderedRoute.route.endsWith('.html')) {
renderedRoute.outputPath = path.join(__dirname, 'dist', renderedRoute.route)
}
return renderedRoute
},
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true
},
renderer: new Renderer({
inject: {
foo: 'bar'
},
maxConcurrentRoutes: 4
]
}
複製代碼
從 5 個大的角度來提高開發效率和體驗,代碼美觀和代碼質量,用戶體驗
* n
mixins
抽離公共邏輯,代碼效率 * n
filter
編碼效率+sass
複用 css
,編碼體驗、效率+eslint + prettier
,代碼風格+、基礎語法錯誤-typescript
,代碼質量+test
,代碼質量+vue
,渲染性能+vuex
減小請求,使用圖片懶加載,加載性能+vue-cli4
,webpack
配置效率+vue-h5-template
,vue
配置效率+pug
,HTML
編寫效率+css
編寫 sass
,CSS
編寫效率+mock
,脫離後端開發效率+HOC
,組件開發,頁面編寫效率+history
使用,服務端配置相關,URL
美觀+SEO
與首屏加載、服務端渲染 SSR
基本解決做者面臨 「失業」 和 「禁足」 在家裏的雙重打擊下,仍然堅持完成了這個系列的最後一篇文章。
若是能對你有幫助,即是它最大的價值。都看到這裏還不點贊,太過不去啦!😄
因爲技術水平有限,文章中若有錯誤地方,請在評論區指出,感謝!
以後做者應該會去鞏固基礎知識,打算寫一個《考古文章系列》,一方面增強本身基礎,方便於面試。另外一方面,想沉下心來面對這剩下 10 多天禁足的日子。但願之後多多關注!