言
前面兩篇文章總結了 Vue 開發的大部分技巧和內容,最后一篇文章來對它進行一個收尾
這篇文章我們來談談一些 Vue 理解和實踐要求高一點的問題
首先是生命周期這一塊內容,隨著實踐越多它的意義越大,理解也越深刻
mixin 功能強大,對代碼復用組織都有很高的要求,算是 Vue 后期發力的高級技巧
服務端渲染可能是學習 Vue 最后一塊陣地了,對于 SPA 框架的一個里程碑
最后,總結一下我在使用 Vue 中使用的技巧和經驗
常規操作,先點贊后觀看哦!你的點贊是我創作的動力之一!
我將從 16 個方面來論述 Vue 開發過程中的一些技巧和原理。如果你還未觀看上節文章,可以移步至
Vue 生命周期大概就是:一個從 Vue 實例的創建到組件銷毀的一個的過程。
具體情況下,我們分為幾個核心的階段,并且每個階段都有一套鉤子函數來執行我們需要的代碼。
我們整理分類一下這些生命周期鉤子,為了記憶方便分為 4 大核心階段:
方便讀者記憶,這里盡量使用圖示:
可以看到每一個階段中的鉤子命名都很好記憶,階段開始前使用 beforeXxx,階段后結束后使用xxxed
除這 8 個核心鉤子,另外還有 3 個新增功能型鉤子,目前總共是 11 個鉤子 順帶提一下這 3 個鉤子的功能
我們看看官方的圖解,在 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鉤子此時觸發了
怎樣觸發銷毀的鉤子呢? 大概有這幾種方法
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 類
當然如果想改變規則,也可以通過配置來改變規則
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 的優點
第一,千萬不能濫用全局 mixins 因為會影響所有多子組件
第二,由于 mixins 的合并策略固有影響,可能在一些極端情況達不到你想要的效果。
比如:我已經存在一個 mixins,我的頁面里面也有一些方法,我要引入mixins 就要做很多改動,來保證我的代碼按我的需要運行。
頁面 data 有一個 message 值,mixins 里面同樣有一個。
按照默認規則,mixins 里面的 message 會被頁面里面 message 覆蓋。但是這兩個 message 可能代表的意義不一樣,都需要存在。那么我就需要改掉其中的一個,如果業務太深的話,可能這個 message 沒那么好改。
有時候需要考慮這些問題,導致使用 mixins 都會增加一些開發負擔。當然也是這些問題可以使用規范來規避。
SSR 是 Serve Side Render 的縮寫,翻譯過來就是我們常說的服務端渲染
但是它還存在以下問題
總得來說,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 呢?其實預渲染也能做到,首先
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 個大的角度來提升開發效率和體驗,代碼美觀和代碼質量,用戶體驗
作者面臨 “失業” 和 “禁足” 在家里的雙重打擊下,仍然堅持完成了這個系列的最后一篇文章。
如果能對你有幫助,便是它最大的價值。都看到這里還不點贊,太過不去啦!
由于技術水平有限,文章中如有錯誤地方,請在評論區指出,感謝!
言
在網絡安全領域,暗鏈接(或隱藏鏈接)是一種常見的網絡欺詐手段。它們通常隱藏在網頁代碼中,對普通用戶不可見,但可能指向惡意網站或用于執行不安全的操作。因此,從網頁源代碼中檢測并識別這些暗鏈接變得尤為重要。本文將探討如何從提取的HTML源代碼中檢測暗鏈接,并將這些鏈接進行輸出。
一、理解暗鏈接
什么是暗鏈接:
暗鏈接通常指的是隱藏在網頁中,對用戶不明顯,但可能含有惡意內容的鏈接。
暗鏈接的常見形式:
完全隱藏的鏈接,如使用CSS將鏈接設為透明或大小為零。
使用微小的文本或圖像作為鏈接。
將鏈接嵌入到不相關元素中。
二、HTML源代碼的檢測方法
分析HTML結構:
介紹如何查看和理解網頁的HTML源代碼。
解釋如何識別HTML中的鏈接元素(通常是<a>標簽)。
檢測暗鏈接的技術:
使用HTML和CSS屬性來識別隱藏的鏈接,如display: none;、opacity: 0;或width: 0px; height: 0px;等。
檢查鏈接的文本內容和尺寸,判斷是否可疑。
三、實現暗鏈接檢測
工具和技術選擇:
介紹可用于解析HTML和CSS的工具,如Python的BeautifulSoup庫。
代碼實現:
提供一個基本的腳本示例,展示如何從HTML中提取鏈接并檢查其屬性以識別暗鏈接。
四、案例分析
實際網頁案例:
選取一個包含暗鏈接的網頁作為案例,展示如何使用工具檢測這些鏈接。
結果展示和分析:
展示檢測到的暗鏈接,并對其可能的風險進行分析。
五、總結與預防建議
總結:
強調檢測暗鏈接在網絡安全中的重要性。
預防建議:
提供一些基本的網絡安全建議,幫助用戶識別和避免訪問暗鏈接。
小結
通過本文的討論,讀者可以了解如何從HTML源代碼中檢測暗鏈接,并了解這些鏈接可能帶來的風險。雖然這需要一定的技術知識,但通過合適的工具和方法,可以有效地識別和防范這種網絡安全威脅。
示例代碼(Python使用BeautifulSoup檢測暗鏈接)
python
from bs4 import BeautifulSoup
import requests
# 加載網頁內容
url="https://example.com"
html_content=requests.get(url).text
soup=BeautifulSoup(html_content, 'html.parser')
# 檢測暗鏈接
dark_links=[]
for link in soup.find_all('a'):
if link.get('style'):
styles=link.get('style').split(';')
if any(s in styles for s in ['display:none', 'opacity:0', 'width:0px', 'height:0px']):
dark_links.append(link.get('href'))
# 輸出檢測到的暗鏈接
print("Detected Dark Links:", dark_links)
這段代碼演示了如何使用BeautifulSoup庫來解析HTML,并檢查每個鏈接的CSS樣式以識別是否為暗鏈接。這只是一個基本的示例,實際應用中可能需要更復雜的邏輯來處理各種隱藏技術。
說之前也是對 chunk 這個概念有些模糊,并且很多時候網上的文章大部分介紹重點在將代碼分離動態加載之類的。寫這篇文章的目的也是想讓其他那些跟我一樣曾經對這個概念不是很清楚的童鞋有個清晰的認識。廢話不多說,開始干!
Let's Dive in!
Webpack 文件分離包括兩個部分,一個是 Bundle 的分離,一個是 Code 代碼的分離:
準備工作
在進行文件分離之前的準備工作,我們先寫一些代碼:
入口文件 src/index.js:
const { getData }=require('./main') const { findMaxIndex }=require('./math') let arr=[1,2,123,21,3,21,321,1] findMaxIndex(arr) getData('./index.html')
兩個依賴模塊:
src/main.js:
const axios=require('axios') const getData=url=> { axios.get(url).then(d=> { console.log(d.status) console.log(d.data.length) }) } module.exports={ getData }
src/math.js:
const _=require('lodash') const findMaxIndex=arr=> { let x=_.max(arr) let r=Array.prototype.indexOf.call(arr, x) console.log(r); } module.exports={ findMaxIndex }
增加一個 webpack 配置文件 webpack.config.js:
const path=require('path') module.exports={ mode: 'development', entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js' }, }
文件分離之前打包效果
在 bundle split 和 code split 操作之前,我們先看一下當前默認打包的效果:
全部依賴都被打包到 main.xxx.js 中去,大小是 609k
開始分離操作
Bundle Split
Bundle Split 的主要任務是將多個引用的包和模塊進行分離,避免全部依賴打包到一個文件下
基本用法
Webpack 4 中需要使用到 optimization.splitChunks 的配置:
const path=require('path') module.exports={ mode: 'development', entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js' }, optimization: { splitChunks: { chunks: 'all' } } }
optimization.splitChunks 的意思是將所有來自 node_modules 中的依賴全部打包分離出來,這個時候我們再看打包的文件是個什么樣子:
增加了 splitChunks 的配置,我們第三方模塊都被打包到了 vendors~main.xxx.js 中去了,這個文件大小有 604k,而入口文件 main.xxx.js 則只有 7k
雖然說這樣將第三方模塊單獨打包出去能夠減小入口文件的大小,但這樣仍然是個不小的文件;這個小的測試項目中我們使用到了 axios 和 lodash 這兩個第三方模塊,因此我們希望的應該是將這兩個模塊單獨分離出來兩個文件,而不是全部放到一個 vendors 中去,那么我們繼續配置 webpack.config.js:
將每個 npm 包單獨分離出來
這里我們需要使用到 webpack.HashedModuleIdsPlugin 這個插件
參考官方文檔(https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks)
直接上代碼:
const path=require('path') const webpack=require('webpack') module.exports={ mode: 'development', entry: path.resolve(__dirname, 'src/index.js'), plugins: [ new webpack.HashedModuleIdsPlugin() // 根據模塊的相對路徑生成 HASH 作為模塊 ID ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js' }, optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', // 默認 async 可選值 all 和 initial maxInitialRequests: Infinity, // 一個入口最大的并行請求數 minSize: 0, // 避免模塊體積過小而被忽略 minChunks: 1, // 默認也是一表示最小引用次數 cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, // 如果需要的依賴特別小,可以直接設置成需要打包的依賴名稱 name(module, chunks, chcheGroupKey) { // 可提供布爾值、字符串和函數,如果是函數,可編寫自定義返回值 const packageName=module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1] // 獲取模塊名稱 return `npm.${packageName.replace('@', '')}` // 可選,一般情況下不需要將模塊名稱 @ 符號去除 } } } } } }
這里我們主要做了幾件事:
為了避免每次打包的文件哈希變化,我們可以使用 webpack 內置的 HashedModuleIdsPlugin,這樣可以避免每次打包的文件哈希值變化首先增加 maxInitialRequests 并設置成 Infinity,指定這個入口文件最大并行請求數然后將 minSize 和 minChunks 分別設置成 0 和 1,即使模塊非常小也將其提取出來,并且這個模塊的引用次數只有 1 也要提取最后配置匹配的依賴以及分離出的文件名格式另外,我們還將運行時代碼分離出來,這塊代碼還可以配合 InlineManifestWebpackPlugin 直接插入到 HTML 文件中。這里我們將這個配置設置成 single,即將所有chunk的運行代碼打包到一個文件中
這樣 Bundle Split 的操作基本就完成了,讓我們看看效果如何:
所依賴的幾個模塊都被分離出去了。
使用 HtmlWebpackPlugin 這個插件將 js 代碼注入 html 文件中。
npm i -D html-webpack-plugin
修改 webpack.config.js 文件:
// 配置文件引入這個插件 var HtmlWebpackPlugin=require('html-webpack-plugin'); // ... module.exports={ // ... plugins: [ new HtmlWebpackPlugin(), new webpack.HashedModuleIdsPlugin() // 根據模塊的相對路徑生成 HASH 作為模塊 ID ], // ... }
安裝 http-server 或使用 vscode 的插件 Live Server 將代碼放入一個本地服務器中,打開瀏覽器的調試窗口進入到 Network 面板:
可以看到我們將模塊單獨分離出來并行加載,這樣比單獨加載一個龐大的包要快不少,接下來我們還要進行代碼分離,將不必要加載的模塊延遲加載
Code Split
代碼分離實際上就是只加載用戶需要使用到的部分代碼,不是必須的就暫時不加載。
這里我們要用到 require.ensure 這個方法去獲取依賴,這樣 webpack 打包之后將會增加運行時代碼,在設定好的條件下才會觸發獲取這個依賴。
在 ES6 中我們可以用 Dynamic Imports 來替代上述方案,如果使用 ES6 語法那么需要使用到 babel 以及 babel 的插件 plugin-syntax-dynamic-import(https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/#installation),在瀏覽器中為了保證兼容性,還需要安裝 promise 的 polyfill,用法大同小異,可直接觀摩 webpack 的官方文檔(https://webpack.js.org/guides/code-splitting/#dynamic-imports)
修改我們的代碼:
const { getData }=require('./main') let arr=[1,2,123,21,3,21,321,1] getData('./index.html') setTimeout(()=> { require.ensure(['./math'], function(require) { const { findMaxIndex }=require('./math') console.log(findMaxIndex(arr)) }) }, 3000)
我們設定了一個定時器,只有在 3000 毫秒以后,瀏覽器才會去請求這個 math 模塊
編譯之后,打開調試面板刷新瀏覽器:
在頁面剛加載完畢后,瀏覽器會嘗試獲取上述這么幾個模塊,因為模塊都很小很快就加載完成了
在 3500ms 左右,瀏覽器才會去獲取 requie.ensure 方法定義的 math 模塊,因 math 模塊又包含依賴 lodash,因此這個 lodash 第三方模塊也會被按需加載。
這樣我們就完成了代碼分離的操作,這樣做的優勢就是不需要第一時間加載的模塊,可以推遲加載,以頁面的加載速度。當然,上面的 timeout 定時器完全可以換成其他諸如按鈕點擊之類的事件來觸發。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。