吃透 Vue 項目開發實踐|16個方面深刻前端工程化開發技巧《下》

前言

前面兩篇文章總結了 Vue 開發的大部分技巧和內容,最後一篇文章來對它進行一個收尾css

這篇文章咱們來談談一些 Vue 理解和實踐要求高一點的問題html

首先是生命週期這一塊內容,隨着實踐越多它的意義越大,理解也越深入前端

mixin 功能強大,對代碼複用組織都有很高的要求,算是 Vue 後期發力的高級技巧vue

服務端渲染多是學習 Vue 最後一塊陣地了,對於 SPA 框架的一個里程碑node

最後,總結一下我在使用 Vue 中使用的技巧和經驗webpack

常規操做,先點贊後觀看哦!你的點贊是我創做的動力之一!git

前情提要

我將從 16 個方面來論述 Vue 開發過程當中的一些技巧和原理。若是你還未觀看上節文章,能夠移步至github

本篇概覽

Vue 生命週期

什麼是 Vue 生命週期?

Vue 生命週期大概就是:一個從 Vue 實例的建立到組件銷燬的一個的過程。web

具體狀況下,咱們分爲幾個核心的階段,而且每一個階段都有一套鉤子函數來執行咱們須要的代碼。面試

生命週期階段與鉤子

咱們整理分類一下這些生命週期鉤子,爲了記憶方便分爲 4 大核心階段:

方便讀者記憶,這裏儘可能使用圖示:

能夠看到每個階段中的鉤子命名都很好記憶,階段開始前使用 beforeXxx,階段後結束後使用xxxed

除這 8 個核心鉤子,另外還有 3 個新增功能型鉤子,目前總共是 11 個鉤子 順帶提一下這 3 個鉤子的功能

  1. 組件緩存activateddeactivated,這兩個鉤子也是一對的,分別表示被 keep-alive 緩存的組件激活和停用時調用。
  2. 組件錯誤捕獲errorCaptured,對組件中出現對異常錯誤進行處理,使用較少。

圖解生命週期

咱們看看官方的圖解,在 Vue 教程實例這節 官方教程直接跳轉

官方圖解

官方圖解並無詳細解釋這張圖。我猜一方面緣由是這個圖裏面涉及的細節都是在 vue 源碼裏面體現,要真正解釋起來也沒那麼簡單。另外一方面是但願咱們多在實踐中去理解裏面的意義。

Vue 源碼基本流程

對於上面那張圖的理解,咱們須要對 Vue 源碼進行梳理,才能真正的理解。大概根據現有的源碼,我梳理了一下大體的流程:

咱們能夠清楚的看到,從 Vue 實例建立、組件掛載、渲染的一些過程當中,有着明顯的週期節點。

簡化文字圖解

結合上面源碼的流程和相關實踐,簡化每個階段作了哪些時期,每個鉤子裏面是組件處於什麼狀態。

實踐驗證一下生命週期

提出問題

下面咱們提出一些問題:

  1. 什麼時期建立 el ?
  2. 什麼時期掛載 data ?
  3. 什麼時期能夠訪問 dom ?
  4. 什麼狀況下組件會更新?更新是同步更新仍是異步更新?
  5. 什麼狀況下組件會被銷燬?
  6. 銷燬組件後,還能夠訪問哪些內容?

編寫代碼

  1. 首先寫一個小 demo,打印關鍵組件信息
<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>
複製代碼
  1. 增長核心的 8 個生命週期鉤子,分別調用打印方法
// ...
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')
},
// ...
複製代碼

建立階段

beforeCreatemethods中方法直接報錯沒法訪問,直接訪問 eldata

發現只能訪問到 watcheldata 均不能訪問到

created 時期 el 沒法訪問到,可是能夠訪問到 data

掛載階段

beforeMount 中能夠訪問 data 可是仍然訪問不到 el

mounted 中能夠訪問到 el

首次加載頁面,更新階段和銷燬階段到鉤子都未觸發

更新階段

咱們增長一行代碼

this.message = this.message + 1
複製代碼

若是增長在 created 階段,發現 update鉤子仍然未觸發,可是 eldata 的值都變成了 2

若是增長在 mounted 階段,發現 update鉤子此時觸發了

銷燬階段

怎樣觸發銷燬的鉤子呢? 大概有這幾種方法

  • 手動調用 $destory
  • v-ifv-for 指令,(v-show 不行)
  • 路由切換和關閉或刷新瀏覽器 咱們在 mounted 鉤子裏面增長一行代碼手動銷燬當前組件,或者跳轉路由
this.$destory('lifecycle')
複製代碼

發現beforeDestorydestoryed 都觸發了,並且eldata都同樣仍是能夠訪問到

生命週期鉤子常見使用的場景

beforeCreate 謹慎操做 this

beforeCreate 沒法訪問到 this 中的 datamethod

// 錯誤實例
beforeCreate() {
// 容許
console.log('ok')
// 不容許
this.print() // 報錯找不到
this.message = 1 // 報錯找不到

}
複製代碼

請求應放在 created 鉤子中

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')
}
複製代碼

操做 DOM 代碼應放在 mounted 鉤子中

mounted 已經掛載 dom,能夠訪問 this

mounted() {
// 容許
this.$el
this.$ref.demo
let a = document.getElementById('')
}
複製代碼

生命週期相關demo 代碼見github-lifecycle-demo

理解併合理使用 mixin

什麼是 mixin(混入)

當組件使用混入對象時,全部混入對象的選項將被「混合」進入該組件自己的選項 大體原理就是將外來的組件、方法以某種方式進行合併。合併的規則有點像繼承和擴展。

當組件和混入對象含有同名選項時,這些選項將以恰當的方式進行「合併」

咱們看一下一個組件裏面有哪些東西是能夠合併的

// mixins/demo
export default {
data() {
return {}
},
mounted() {},
methods: {},
computed: {},
components: {},
directives: {}
}
複製代碼

datamethodscomputeddirectivescomponents 生命週期鉤子

沒錯這些均可以混入

import demoMixin form '@/mixins/demo'
export default {
mixins: [demoMixin]
}
複製代碼

這樣看來,不少頁面重複的代碼咱們均可以直接抽取出來

或者是封裝成一個公共的 mixin

好比咱們作 H5 頁面,裏面不少短信驗證的邏輯固有邏輯,可是須要訪問到 this。使用工具函數確定不行。

這時候就能夠考慮使用 mixin,封裝成一個具備響應式的模塊。供須要的地方進行引入。

mixin 規則

首先是優先級的問題,當重名選項時選擇哪個爲最後的結果

默認規則我這裏分爲 3

  1. data 混入: 以當前組件值爲最後的值
  2. 生命週期鉤子: 保留全部鉤子,先執行 mixins 的,後執行當前組件的
  3. methodscomputeddirectivescomponents 這種健值對形式,同名key,通通以當前組件爲準

固然若是想改變規則,也能夠經過配置來改變規則

Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合併後的值
}
複製代碼

mixin 的好處

咱們知道 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 的合併策略固有影響,可能在一些極端狀況達不到你想要的效果

好比:我已經存在一個 mixins,個人頁面裏面也有一些方法,我要引入mixins 就要作不少改動,來保證個人代碼按個人須要運行。

頁面 data 有一個 message 值,mixins 裏面一樣有一個。

按照默認規則,mixins 裏面的 message 會被頁面裏面 message 覆蓋。可是這兩個 message 可能表明的意義不同,都須要存在。那麼我就須要改掉其中的一個,若是業務太深的話,可能這個 message 沒那麼好改。

有時候須要考慮這些問題,致使使用 mixins 都會增長一些開發負擔。固然也是這些問題可使用規範來規避。

理解並使用 SSR

SSR 是 Serve Side Render 的縮寫,翻譯過來就是咱們常說的服務端渲染

簡單概括 SSR 存在的緣由

  1. SPA 框架 SEO 的解決方案
  2. 提高首屏加載速度

可是它還存在如下問題

  • 有些生命週期鉤子沒法使用(以前提到的 activateddeactivated 等等)
  • 額外不少的配置
  • 服務器資源的需求

總得來講,SSR 是必要的但不是充分的,SPA 的 SEO 如今沒有更好的方案,有這方面強烈需求的網站來講,SSR 確實頗有必要

SSR 原理

經過上面圖咱們能夠得大體幾點內容

  • 通用業務代碼只寫一套。
  • 經過暴露客戶端和服務端兩個入口,對 Vue 實例進行訪問。
  • 經過構建工具打成兩個包,服務端包經過 node 服務器渲染給瀏覽器訪問。客戶端和原來同樣經過訪問資源獲取。

實現SSR 有 3 種方式

1. 根據官網教程搭建,一步一步搭建SSR, 這裏有具體教程,略微有點麻煩,可是可以體驗搭建的過程,更加深刻細節。使用於練習,和現有項目的改造

2. 使用demo改造,開源 demo,方便省時,適用代碼參考學習 vue-ssr-demo

3. 使用nuxt,預設了 SSR 和預渲染,適用於須要 SSR 的新項目。

分別嘗試用這 3 種方式搭建 SSR

五步簡單理解SSR(根據官網教程,具體完操做查看整教程和demo)

這裏主要加深理解,vue-cli3+ 實現基本 SSR

第一步:安裝依賴

  • vue-server-renderer (核心依賴,版本必須與 vue 版本一致)
  • webpack-merge(用於webpack配置合併)
  • webpack-node-externals (用於webpack配置更改)
  • express (用於服務端渲染)

第二步:創建入口,並改造改造

分爲 2 個入口,將 main.js 定爲通用入口, 並額外增長entry-client.jsentry-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);
});
};
複製代碼

第三步:改造 vue.config 配置

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()]
}),
//...
};
複製代碼

第四步:對路由 router 改造

export function createRouter(){
return new Router({
mode: 'history',
routes: [
//...
]
})
}
複製代碼

第五步:使用 express 運行服務端代碼

這一步主要是讓 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 把咱們以前提到的重要的改造,所有封裝到 .nuxt 文件夾裏面了

跑一下 dev 發現有兩個端,一個 clinet 端,一個 server 端

最後查看一下效果,整個過程挺絲滑的。目錄結構也比較符合個人風格,新項目須要 SSR 會考慮使用 nuxt

使用 webpack 插件預渲染

解決 SEO 問題是否是隻有 SSR 呢?其實預渲染也能作到,首先

區別使用 SSR 和預渲染

  • 服務端渲染解決的問題,不只只是把 HTML 頁面給瀏覽器,更重要的是處理動態邏輯和 JS 代碼後,將渲染後完整的 HTML 給瀏覽器,渲染的過程在服務端。
  • 預渲染,是利用構建工具在 webpack 中生成靜態的 HTML,直接給瀏覽器,渲染的過程在本地。
  • 預渲染插件裏面提到兩種不能使用:大量路由、動態內容

使用 prerender-spa-plugin 插件進行簡單預渲染

  1. 安裝 prerender-spa-plugin
yarn prerender-spa-plugin
複製代碼
  1. 修改 webpack 配置,比較簡單就能完成配置
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
]
}
複製代碼

Vue 開發技巧總結

從 5 個大的角度來提高開發效率體驗代碼美觀代碼質量用戶體驗

5 大角度提高

代碼複用

  • 組件化開發,代碼效率 * n
  • 使用 mixins 抽離公共邏輯,代碼效率 * n
  • 工具化函數、使用 filter 編碼效率+
  • sass 複用 css,編碼體驗、效率+

代碼質量

  • 代碼靜態檢查 eslint + prettier,代碼風格+、基礎語法錯誤-
  • 數據類型控制 typescript,代碼質量+
  • 前端測試 test,代碼質量+

代碼優化

  • 合理使用 vue,渲染性能+
  • 合理使用 vuex 減小請求,使用圖片懶加載,加載性能+
  • 合理使用函數組件,組件性能+
  • 合理骨架屏、路由過渡,用戶體驗+

開發效率

  • 使用更新的腳手架 vue-cli4webpack 配置效率+
  • 使用配置好的腳手架模版 vue-h5-templatevue 配置效率+
  • 使用更簡潔的模版 pugHTML 編寫效率+
  • 使用更強大的 css 編寫 sassCSS 編寫效率+
  • 使用模擬數據 mock,脫離後端開發效率+
  • 開源組件封裝 HOC,組件開發,頁面編寫效率+

瓶頸解決

  • 路由history使用,服務端配置相關,URL美觀+
  • 解決SEO與首屏加載、服務端渲染 SSR 基本解決

後記

做者面臨 「失業」 和 「禁足」 在家裏的雙重打擊下,仍然堅持完成了這個系列的最後一篇文章。

若是能對你有幫助,即是它最大的價值。都看到這裏還不點贊,太過不去啦!😄

因爲技術水平有限,文章中若有錯誤地方,請在評論區指出,感謝!

以後做者應該會去鞏固基礎知識,打算寫一個《考古文章系列》,一方面增強本身基礎,方便於面試。另外一方面,想沉下心來面對這剩下 10 多天禁足的日子。但願之後多多關注!

相關文章
相關標籤/搜索