<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
隨著前端應用的逐步複雜,我們的元件中需要使用越來越多的狀態。有的時候我們需要使用子元件將狀態傳遞給父元件就會比較複雜,資料的向上傳遞過程我們可能會使用回撥函數或是資料繫結的形式去處理,就會讓程式碼晦澀難懂。
我們需要一種方式,能夠讓資料在所有元件中共用,同時能以簡單的方式進行傳遞,這種組織資料的方式就是狀態管理。我們很自然的就想到,把資料放到所有需要使用的元件的公共祖先上,在使用時自上而下傳遞即可。
在 vue.js
中,我們主要說的狀態管理庫就是 vuex
,當然,只要你能實現有條理的組織資料,那麼它都可以認為是一種狀態管理庫。
事實上,我們可以簡單的這樣理解【狀態管理】這個詞,vuex
實際上做的事情就是:
store
,將所有元件間需要共用的資料放置於此;store
內的資料進行更新,同時更新完之後響應式更新所有使用此資料元件的檢視;srcutil.js
export function find(list, f) { return list.filter(f)[0]; } export function deepCopy(obj, cache = []) { if (obj === null || typeof obj !== 'object') { return obj; } const hit = find(cache, c => c.original === obj); if (hit) { return hit.copy; } const copy = Array.isArray(obj) ? [] : {}; cache.push({ original: obj, copy, }); Object.keys(obj).forEach(key => { copy[key] = deepCopy(obj[key], cache); }); return copy; } export function forEachValue(obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key)); } export function isObject(obj) { return obj !== null && typeof obj === 'object'; } export function isPromise(val) { return val && typeof val.then === 'function'; } export function assert(condition, msg) { if (!condition) throw new Error(`[vuex] ${msg}`); } export function partial(fn, arg) { return function () { return fn(arg); }; }
在說 vuex
之前,我們必須說一說 flux
架構,flux
架構可謂是狀態管理的鼻祖。
flux
架構最早由 facebook
推出,主要是為了處理當時他們的 react
框架下狀態管理的問題,但在當時來講,整個設計比較複雜,後來人們簡化了其中的一些理念,但是保留了核心思想,繼而依據框架實現了很多不同的狀態管理庫,例如 redux
,vuex
等等。其中 redux
大多數被用在了 react
專案中,而 vuex
就是在 vue
框架中實現的這麼一個 flux
架構的狀態管理庫。
**flux
架構約定,存放資料的地方稱為 store
,store
內部的 state
是資料本身,我們必須通過 action
才能修改 store
裡的 state
。**這裡的 action
指的意思是 行為,在大部分實現裡面是一個函數,通過呼叫函數來更改 store
內部的 state
。
vuex
中,我們可以通過 mutation
來【同步】的改變 state
,這時候就可以在元件中通過 commit
進行呼叫更改 state
。
同樣的,我們也可以通過 action
來【非同步】更改 state
,不過在 action
中,我們還是需要呼叫 mutation
。
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, modules: {}, });
./store/index.js
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { count: 0, }, mutations: { increment(state, payload = 1) { state.count += payload; }, }, actions: {}, modules: {}, });
./Home.vue
<template> <div class="home"> <div>count: {{ count }}</div> <button @click="increment">增加</button> <button @class="decrement">減少</button> </div> </template> <script> export default { name: 'Home', computed: { count() { return this.$store.state.count; }, }, methods: { increment() { // 使用 commit 派發 mutation 事件 // this.$store.commit("increment"); // 可以傳遞引數 this.$store.commit('increment', 2); }, decrement() {}, }, }; </script>
可以使用計算屬性獲取 state
中的資料:
computed: { count () { return this.$store.state.count } }
當一個元件需要獲取多個狀態的時候,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,我們可以使用 mapState
輔助函數幫助我們生成計算屬性。
import { mapState } from "vuex"; computed: { ...mapState(["num"]) }
Vuex
允許我們在 store
中定義 getter
(可以認為是 store
的計算屬性)。就像計算屬性一樣,getter
的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。
Getter
接受 state
作為其第一個引數:
const store = new Vuex.Store({ state: { todos: [ { id: 1, text: 'foo', done: true }, { id: 2, text: 'bar', done: false }, ], }, getters: { doneTodos: state => { return state.todos.filter(todo => todo.done); }, }, });
Getter
會暴露為 store.getters
物件,你可以以屬性的形式存取這些值:
store.getters.doneTodos; // -> [{ id: 1, text: '...', done: true }]
Getter
也可以接受其他 getter
作為第二個引數:
getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length; }; }
你也可以通過讓 getter
返回一個函數,來實現給 getter
傳參。
getters: { // ... getTodoById: state => id => { return state.todos.find(todo => todo.id === id); }; }
store.getters.getTodoById(2); // -> { id: 2, text: '...', done: false }
【注意】:getter
在通過方法存取時,每次都會去進行呼叫,而不會快取結果。
mapGetters
輔助函數僅僅是將 store
中的 getter
對映到區域性計算屬性:
import { mapGetters } from 'vuex'; export default { computed: { // 使用物件展開運運算元將 getter 混入 computed 物件中 ...mapGetters(['doneTodosCount', 'anotherGetter']), }, };
如果你想將一個 getter
屬性另取一個名字,使用物件形式:
...mapGetters({ // 把 this.doneCount 對映為 this.$store.getters.doneTodosCount doneCount: 'doneTodosCount' })
更改 Vuex
的 store
中的狀態的唯一方法是提交 mutation
。Vuex 中的 mutation
非常類似於事件:每個 mutation
都有一個字串的 事件型別 (type
) 和 一個 回撥函數 (handler
)。這個回撥函數就是我們實際進行狀態更改的地方,並且它會接受 state
作為第一個引數:
const store = new Vuex.Store({ state: { count: 1, }, mutations: { increment(state) { // 變更狀態 state.count++; }, }, });
你不能直接呼叫一個 mutation handler
。這個選項更像是事件註冊:“當觸發一個型別為 increment
的 mutation
時,呼叫此函數。”要喚醒一個 mutation handler
,你需要以相應的 type
呼叫 store.commit
方法:
store.commit('increment');
你可以向 store.commit
傳入額外的引數,即 mutation
的 載荷(payload
):
mutations: { increment (state, n) { state.count += n } } // 使用方法 store.commit('increment', 10)
在大多數情況下,載荷應該是一個物件,這樣可以包含多個欄位並且記錄的 mutation
會更易讀:
mutations: { increment (state, payload) { state.count += payload.amount } } // 使用方法 store.commit('increment', { amount: 10 })
物件風格的提交方式:
提交 mutation
的另一種方式是直接使用包含 type
屬性的物件:
store.commit({ type: 'increment', amount: 10, });
當使用物件風格的提交方式,整個物件都作為載荷傳給 mutation
函數,因此 handler
保持不變:
mutations: { increment (state, payload) { state.count += payload.amount } }
使用常數替代 mutation
事件型別在各種 Flux
實現中是很常見的模式。這樣可以使 linter
之類的工具發揮作用,同時把這些常數放在單獨的檔案中可以讓你的程式碼合作者對整個 app
包含的 mutation
一目瞭然:
// mutation-types.js export const SOME_MUTATION = 'SOME_MUTATION';
// store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 我們可以使用 ES2015 風格的計算屬性命名功能來使用一個常數作為函數名 [SOME_MUTATION] (state) { // mutate state } } })
一條重要的原則就是要記住 mutation
必須是同步函數。為什麼?請參考下面的例子:
mutations: { someMutation (state) { api.callAsyncMethod(() => { state.count++ }) } }
現在想象,我們正在 debug
一個 app
並且觀察 devtool
中的 mutation
紀錄檔。每一條 mutation
被記錄,devtools
都需要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation
中的非同步函數中的回撥讓這不可能完成:因為當 mutation
觸發的時候,回撥函數還沒有被呼叫,devtools
不知道什麼時候回撥函數實際上被呼叫 —— 實質上任何在回撥函數中進行的狀態的改變都是不可追蹤的。
你可以在元件中使用 this.$store.commit('xxx')
提交 mutation
,或者使用 mapMutations
輔助函數將元件中的 methods
對映為 store.commit
呼叫(需要在根節點注入 store
)。
import { mapMutations } from 'vuex'; export default { // ... methods: { ...mapMutations([ 'increment', // 將 `this.increment()` 對映為 `this.$store.commit('increment')` // `mapMutations` 也支援載荷: 'incrementBy', // 將 `this.incrementBy(amount)` 對映為 `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: 'increment', // 將 `this.add()` 對映為 `this.$store.commit('increment')` }), }, };
Action
類似於 mutation
,不同在於:
Action
提交的是 mutation
,而不是直接變更狀態。Action
可以包含任意非同步操作。讓我們來註冊一個簡單的 action
:
const store = new Vuex.Store({ state: { count: 0, }, mutations: { increment(state) { state.count++; }, }, actions: { increment(context) { context.commit('increment'); }, }, });
Action
函數接受一個與 store
範例具有相同方法和屬性的 context
物件,因此你可以呼叫 context.commit
提交一個 mutation
,或者通過 context.state
和 context.getters
來獲取 state
和 getters
。當我們在之後介紹到 Modules 時,你就知道 context
物件為什麼不是 store
範例本身了。
實踐中,我們會經常用到 ES2015
的引數解構來簡化程式碼(特別是我們需要呼叫 commit
很多次的時候):
actions: { increment ({ commit, state, getters }) { commit('increment') } }
Action
通過 store.dispatch
方法觸發:
store.dispatch('increment');
乍一眼看上去感覺多此一舉,我們直接分發 mutation
豈不更方便?實際上並非如此,還記得 mutation
必須同步執行這個限制麼?Action
就不受約束!我們可以在 action
內部執行非同步操作:
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
Actions
支援同樣的載荷方式和物件方式進行分發:
// 以載荷形式分發 store.dispatch('incrementAsync', { amount: 10, }); // 以物件形式分發 store.dispatch({ type: 'incrementAsync', amount: 10, });
你在元件中使用 this.$store.dispatch('xxx')
分發 action
,或者使用 mapActions
輔助函數將元件的 methods
對映為 store.dispatch
呼叫(需要先在根節點注入 store
):
import { mapActions } from 'vuex'; export default { // ... methods: { ...mapActions([ 'increment', // 將 `this.increment()` 對映為 `this.$store.dispatch('increment')` // `mapActions` 也支援載荷: 'incrementBy', // 將 `this.incrementBy(amount)` 對映為 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions({ add: 'increment', // 將 `this.add()` 對映為 `this.$store.dispatch('increment')` }), }, };
Action
通常是 非同步 的,那麼如何知道 action
什麼時候結束呢?更重要的是,我們如何才能組合多個 action
,以處理更加複雜的非同步流程?
首先,你需要明白 store.dispatch
可以處理被觸發的 action
的處理常式返回的 Promise
,並且 store.dispatch
仍舊返回 Promise
:
actions: { actionA ({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit('someMutation') resolve() }, 1000) }) } }
現在你可以:
store.dispatch('actionA').then(() => { // ... });
在另外一個 action
中也可以:
actions: { // ... actionB ({ dispatch, commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) } }
最後,如果我們利用 async / await
,我們可以如下組合 action
:
// 假設 getData() 和 getOtherData() 返回的是 Promise actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, commit }) { await dispatch('actionA') // 等待 actionA 完成 commit('gotOtherData', await getOtherData()) } }
一個
store.dispatch
在不同模組中可以觸發多個action
函數。在這種情況下,只有當所有觸發函數完成後,返回的Promise
才會執行。
開啟嚴格模式,僅需在建立 store
的時候傳入 strict: true
:
const store = new Vuex.Store({ // ... strict: true, });
在嚴格模式下,無論何時發生了狀態變更且不是由 mutation
函數引起的,將會丟擲錯誤(但是資料還是會改變)。這能保證所有的狀態變更都能被偵錯工具跟蹤到。
不要在釋出環境下啟用嚴格模式! 嚴格模式會深度監測狀態樹來檢測不合規的狀態變更 —— 請確保在釋出環境下關閉嚴格模式,以避免效能損失。
類似於外掛,我們可以讓構建工具來處理這種情況:
const store = new Vuex.Store({ // ... strict: process.env.NODE_ENV !== 'production', });
大多數我們使用的 UI
框架如 vue
和 react
,都是在使用者端進行渲染,也就是說每個使用者在載入進來我們所有的 html
檔案和 js
檔案之後,才開始渲染頁面的內容。
但是這樣做會有兩個問題,一個是如果使用者網路速度比較慢,如果我們渲染的內容比較多的話,就會產生一個延遲,造成不好的使用者體驗。另一個是某些爬蟲,例如百度的搜尋收錄的爬蟲,在爬取你的頁面時,獲取不到你的頁面的真實內容,導致站點 SEO
權重變低。
所以很多需要 SEO
的頁面,都需要在伺服器端提前渲染好 html
的內容,在使用者存取時先返回給使用者內容,這楊對使用者和爬蟲都非常友好。
我們可以通過直接在頁面上右擊檢視網頁原始碼,來檢視一個頁面是否有伺服器端渲染。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>使用者端渲染</title> </head> <body> <script> document.body.innerHTML = '<div>你好</div>'; </script> </body> </html>
在 Network
的中 Preview
中無資料,在 Response
中的沒有 DOM 標籤:
檢視網頁原始碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>伺服器端渲染</title> </head> <body> <div>你好</div> </body> </html>
在 Network
的中 Preview
中有資料,在 Response
中的有 DOM 標籤:
檢視網頁原始碼:
在控制檯中可以看到,切換路由的時候並沒有發起 ajax
請求。
vue.js
的伺服器端渲染非常簡單,我們只需要在 node.js
中通過 vue-server-renderer
模組,呼叫對應伺服器端渲染的渲染器對元件渲染即可,他就會生成元件對應的 html
內容。渲染成功的 html
標籤,我們可以直接返回到使用者端作為初始請求 html
的返回值。
yarn add express vue vue-server-renderer
./index.js
const Vue = require('vue'); const createRenderer = require('vue-server-renderer').createRenderer; const vm = new Vue({ data() { return { count: 100, }; }, template: `<div>{{ count }}</div>`, }); const renderer = createRenderer(); renderer.renderToString(vm, (err, html) => { console.log('html ==========', html); // <div data-server-rendered="true">100</div> });
./index.js
const Vue = require('vue'); const createRenderer = require('vue-server-renderer').createRenderer; const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); // ! 伺服器端路由 app.get('*', function (req, res) { const vm = new Vue({ data() { return { url: `伺服器端路由 ${req.url}`, count: 100, }; }, template: `<div>{{ url }} - {{ count }}</div>`, }); const renderer = createRenderer({ // 設定模板 template: fs.readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8'), }); renderer.renderToString(vm, (err, html) => { res.send(html); }); }); const PORT = 8080; app.listen(PORT, () => { console.log(`伺服器啟動在 ${PORT} 埠`); });
./index.template.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>伺服器端渲染Demo</title> </head> <body> <h1>這裡是模板</h1> <!--! 中間不能帶空格 --> <!--vue-ssr-outlet--> </body> </html>
index.js
檔案:node ./index.js
我們需要注意的一點是,在伺服器端渲染元件,我們使用不了 window
、location
等瀏覽器環境中的物件,所以如果元件內部使用了這種內容會報錯。
同時,在伺服器端渲染時我們要注意,元件的生命週期也會只執行 beforeCreate
和 created
這兩個,所以在此宣告週期裡面不能使用 window
,但是可以在其他宣告週期比如 mounted
中使用。還有渲染的資料,對於伺服器端渲染的元件來說,我們不應該發請求獲取元件資料,而是應該直接渲染時使用資料進行渲染。
路由也是如此,在 vue
使用者端使用路由的時候,我們也需要在伺服器端對路由進行匹配,從而得知具體需要渲染的元件是哪個。
參考檔案(
Vue
檔案中 -Vue
伺服器端渲染):ssr.vuejs.org/zh/
./webpack.config.js
/* 使用者端 webpack 設定 */ const path = require('path'); const webpack = require('webpack'); module.exports = { entry: './src/entry-client.js', output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js', }, module: { rules: [ { test: /.vue$/, loader: 'vue-loader', }, { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['*', '.js', '.vue'], }, };
./webpack.server.config.js
/* 伺服器端 webpack 設定 */ const path = require('path'); const webpack = require('webpack'); const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.config'); const config = merge(baseWebpackConfig, { target: 'node', entry: { app: './src/entry-server.js', }, output: { path: __dirname, filename: 'server.bundle.js', libraryTarget: 'commonjs2', }, }); console.log('config ============ ', config); module.exports = config;
./package.json
{ "name": "06", "version": "1.0.0", "description": "", "main": "webpack.config.js", "dependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.5", "babel-preset-env": "^1.7.0", "babel-preset-stage-3": "^6.24.1", "express": "^4.17.1", "vue": "^2.6.11", "vue-router": "^3.3.2", "vue-server-renderer": "^2.6.11", "vuex": "^3.4.0", "webpack": "^3.12.0", "webpack-merge": "^4.2.2" }, "devDependencies": { "vue-loader": "^13.7.3", "vue-template-compiler": "^2.6.11" }, "scripts": { "build-server": "webpack --config webpack.server.config.js", "build-client": "webpack --config webpack.config.js" }, "keywords": [], "author": "", "license": "ISC" }
./src/App.vue
<template> <div> <h1>this is App.vue</h1> <router-link to="/">root</router-link> <router-link to="/about">about</router-link> <router-view></router-view> </div> </template>
./src/Home.vue
<template> <div>Home {{ $store.state.timestamp }}</div> </template>
./src/About.vue
<template> <div>about {{ $store.state.timestamp }}</div> </template>
./index.js
/* entry-client 和 entry-server 共同的檔案 */ import Vue from 'vue'; import Router from 'vue-router'; import Vuex from 'vuex'; import Home from './Home'; import About from './About'; import App from './App'; Vue.use(Router); Vue.use(Vuex); export function createApp() { const store = new Vuex.Store({ state: { timestamp: new Date().getTime(), }, }); if (typeof window !== 'undefined' && window.store) { store.replaceState(window.store); } const router = new Router({ mode: 'history', routes: [ { path: '/', component: Home }, { path: '/about', component: About }, ], }); const vm = new Vue({ router, store, render: h => h(App), }); return { vm, router, store }; }
./src/entry-server.js
第一種/* 伺服器端渲染 - 入口 */ const express = require('express'); const fs = require('fs'); const path = require('path'); const renderer = require('vue-server-renderer').createRenderer(); const { createApp } = require('./index'); const app = express(); app.use('/dist', express.static(path.join(__dirname, './dist'))); app.get('/build.js', function (req, res) { const pathUrl = path.resolve(process.cwd(), './dist/build.js'); console.log(pathUrl); res.sendFile(pathUrl); }); app.get('*', function (req, res) { const url = req.url; const { vm, router } = createApp(); router.push(url); /* const matchedComponents: Array<Component> = router.getMatchedComponents(location?) 返回目標位置或是當前路由匹配的元件陣列 (是陣列的定義/構造類,不是範例)。通常在伺服器端渲染的資料預載入時使用。 */ const matchedComponent = router.getMatchedComponents(); if (!matchedComponent) { // 404 處理 } else { renderer.renderToString(vm, function (err, html) { res.send(html); }); } }); const PORT = 8080; app.listen(PORT, () => { console.log(`伺服器啟動在 ${PORT} 埠`); }); /* 此時可以執行 yarn build-server 編譯 entry-server 檔案,生成 server.bundle.js 執行 node ./server.bundle.js 檢視伺服器端路由的結果 */
./src/entry-server.js
第二種/* 伺服器端渲染 - 入口 */ const express = require('express'); const fs = require('fs'); const path = require('path'); const renderer = require('vue-server-renderer').createRenderer({ template: fs.readFileSync(path.resolve(process.cwd(), './index.template.html'), 'utf-8'), }); const { createApp } = require('./index'); const app = express(); app.use('/dist', express.static(path.join(__dirname, './dist'))); app.get('/build.js', function (req, res) { const pathUrl = path.resolve(process.cwd(), './dist/build.js'); console.log(pathUrl); res.sendFile(pathUrl); }); app.get('*', function (req, res) { const url = req.url; const { vm, router, store } = createApp(); router.push(url); /* const matchedComponents: Array<Component> = router.getMatchedComponents(location?) 返回目標位置或是當前路由匹配的元件陣列 (是陣列的定義/構造類,不是範例)。通常在伺服器端渲染的資料預載入時使用。 */ const matchedComponent = router.getMatchedComponents(); if (!matchedComponent) { // 404 處理 } else { renderer.renderToString(vm, function (err, html) { res.send(html); }); } }); const PORT = 8080; app.listen(PORT, () => { console.log(`伺服器啟動在 ${PORT} 埠`); });
./src/entry-client.js
/* 使用者端渲染 - 入口 */ import { createApp } from './index'; const { vm } = createApp(); vm.$mount('#app');
./index.template.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"> <h1>這裡是模板</h1> <!--vue-ssr-outlet--> </div> <script src="/build.js"></script> </body> </html>
執行 yarn build-client
編譯使用者端;
執行 yarn build-server
編譯伺服器端;
執行 node ./server.bundle.js
啟動伺服器,開啟瀏覽器輸入網址 http://localhost:8080/
;
到此這篇關於Vue.js 狀態管理及 SSR解析的文章就介紹到這了,更多相關Vue.js SSR內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45