前言
最近在看 webpack 如何做持久化緩存的內(nèi)容,發(fā)現(xiàn)其中還是有一些坑點(diǎn)的,正好有時(shí)間就將它們整理總結(jié)一下,讀完本文你大致能夠明白:
什么是持久化緩存,為什么做持久化緩存?
webpack 如何做持久化緩存?
webpack 做緩存的一些注意點(diǎn)。
持久化緩存
首先我們需要去解釋一下,什么是持久化緩存,在現(xiàn)在前后端分離的應(yīng)用大行其道的背景下,前端 html,css,js 往往是以一種靜態(tài)資源文件的形式存在于服務(wù)器,通過(guò)接口來(lái)獲取數(shù)據(jù)來(lái)展示動(dòng)態(tài)內(nèi)容。這就涉及到公司如何去部署前端代碼的問(wèn)題,所以就涉及到一個(gè)更新部署的問(wèn)題,是先部署頁(yè)面,還是先部署資源?
先部署頁(yè)面,再部署資源:在二者部署的時(shí)間間隔內(nèi),如果有用戶(hù)訪問(wèn)頁(yè)面,就會(huì)在新的頁(yè)面結(jié)構(gòu)中加載舊的資源,并且把這個(gè)舊版本資源當(dāng)做新版本緩存起來(lái),其結(jié)果就是:用戶(hù)訪問(wèn)到一個(gè)樣式錯(cuò)亂的頁(yè)面,除非手動(dòng)去刷新,否則在資源緩存過(guò)期之前,頁(yè)面會(huì)一直處于錯(cuò)亂的狀態(tài)。
先部署資源,再部署頁(yè)面:在部署時(shí)間間隔內(nèi),有舊版本的資源本地緩存的用戶(hù)訪問(wèn)網(wǎng)站,由于請(qǐng)求的頁(yè)面是舊版本,資源引用沒(méi)有改變,瀏覽器將直接使用本地緩存,這樣屬于正常情況,但沒(méi)有本地緩存或者緩存過(guò)期的用戶(hù)在訪問(wèn)網(wǎng)站的時(shí)候,就會(huì)出現(xiàn)舊版本頁(yè)面加載新版本資源的情況,導(dǎo)致頁(yè)面執(zhí)行錯(cuò)誤。
所以我們需要一種部署策略來(lái)保證在更新我們線上的代碼的時(shí)候,線上用戶(hù)也能平滑地過(guò)渡并且正確打開(kāi)我們的網(wǎng)站。
推薦先看這個(gè)回答:大公司里怎樣開(kāi)發(fā)和部署前端代碼?
當(dāng)你讀完上面的回答,大致就會(huì)明白,現(xiàn)在比較成熟的持久化緩存方案就是在靜態(tài)資源的名字后面加 hash 值,因?yàn)槊看涡薷奈募傻?hash 值不一樣,這樣做的好處在于增量式發(fā)布文件,避免覆蓋掉之前文件從而導(dǎo)致線上的用戶(hù)訪問(wèn)失效。
因?yàn)橹灰龅矫看伟l(fā)布的靜態(tài)資源(css, js, img)的名稱(chēng)都是獨(dú)一無(wú)二的,那么我就可以:
針對(duì) html 文件:不開(kāi)啟緩存,把 html 放到自己的服務(wù)器上,關(guān)閉服務(wù)器的緩存,自己的服務(wù)器只提供 html 文件和數(shù)據(jù)接口
針對(duì)靜態(tài)的 js,css,圖片等文件:開(kāi)啟 cdn 和緩存,將靜態(tài)資源上傳到 cdn 服務(wù)商,我們可以對(duì)資源開(kāi)啟長(zhǎng)期緩存,因?yàn)槊總€(gè)資源的路徑都是獨(dú)一無(wú)二的,所以不會(huì)導(dǎo)致資源被覆蓋,保證線上用戶(hù)訪問(wèn)的穩(wěn)定性。
每次發(fā)布更新的時(shí)候,先將靜態(tài)資源(js, css, img) 傳到 cdn 服務(wù)上,然后再上傳 html 文件,這樣既保證了老用戶(hù)能否正常訪問(wèn),又能讓新用戶(hù)看到新的頁(yè)面。
上面大致介紹了下主流的前端持久化緩存方案,那么我們?yōu)槭裁葱枰龀志没彺婺兀?/p>
用戶(hù)使用瀏覽器第一次訪問(wèn)我們的站點(diǎn)時(shí),該頁(yè)面引入了各式各樣的靜態(tài)資源,如果我們能做到持久化緩存的話,可以在 http 響應(yīng)頭加上 Cache-control 或 Expires 字段來(lái)設(shè)置緩存,瀏覽器可以將這些資源一一緩存到本地。
用戶(hù)在后續(xù)訪問(wèn)的時(shí)候,如果需要再次請(qǐng)求同樣的靜態(tài)資源,且靜態(tài)資源沒(méi)有過(guò)期,那么瀏覽器可以直接走本地緩存而不用再通過(guò)網(wǎng)絡(luò)請(qǐng)求資源。
webpack 如何做持久化緩存
上面簡(jiǎn)單介紹完持久化緩存,下面這個(gè)才是重點(diǎn),那么我們應(yīng)該如何在 webpack 中進(jìn)行持久化緩存的呢,我們需要做到以下兩點(diǎn):
保證 hash 值的唯一性,即為每個(gè)打包后的資源生成一個(gè)獨(dú)一無(wú)二的 hash 值,只要打包內(nèi)容不一致,那么 hash 值就不一致。
保證 hash 值的穩(wěn)定性,我們需要做到修改某個(gè)模塊的時(shí)候,只有受影響的打包后文件 hash 值改變,與該模塊無(wú)關(guān)的打包文件 hash 值不變。
hash 文件名是實(shí)現(xiàn)持久化緩存的第一步,目前 webpack 有兩種計(jì)算 hash 的方式([hash] 和 [chunkhash])
hash 代表每次 webpack 在編譯的過(guò)程中會(huì)生成唯一的 hash 值,在項(xiàng)目中任何一個(gè)文件改動(dòng)后就會(huì)被重新創(chuàng)建,然后 webpack 計(jì)算新的 hash 值。
chunkhash 是根據(jù)模塊計(jì)算出來(lái)的 hash 值,所以某個(gè)文件的改動(dòng)只會(huì)影響它本身的 hash 值,不會(huì)影響其他文件。
所以如果你只是單純地將所有內(nèi)容打包成同一個(gè)文件,那么 hash 就能夠滿(mǎn)足你了,如果你的項(xiàng)目涉及到拆包,分模塊進(jìn)行加載等等,那么你需要用 chunkhash,來(lái)保證每次更新之后只有相關(guān)的文件 hash 值發(fā)生改變。
所以我們?cè)谝环菥哂谐志没彺娴?webpack 配置應(yīng)該長(zhǎng)這樣:
module.exports = { entry: __dirname + '/src/index.js', output: { path: __dirname + '/dist', filename: '[name].[chunkhash:8].js', } }
上面代碼的含義就是:以 index.js 為入口,將所有的代碼全部打包成一個(gè)文件取名為 index.xxxx.js 并放到 dist 目錄下,現(xiàn)在我們可以在每次更新項(xiàng)目的時(shí)候做到生成新命名的文件了。
如果是應(yīng)付簡(jiǎn)單的場(chǎng)景,這樣做就夠了,但是在大型多頁(yè)面應(yīng)用中,我們往往需要對(duì)頁(yè)面進(jìn)行性能優(yōu)化:
分離業(yè)務(wù)代碼和第三方的代碼:之所以將業(yè)務(wù)代碼和第三方代碼分離出來(lái),是因?yàn)闃I(yè)務(wù)代碼更新頻率高,而第三方代碼更新迭代速度慢,所以我們將第三方代碼(庫(kù),框架)進(jìn)行抽離,這樣可以充分利用瀏覽器的緩存來(lái)加載第三方庫(kù)。
按需加載:比如在使用 React-Router 的時(shí)候,當(dāng)用戶(hù)需要訪問(wèn)到某個(gè)路由的時(shí)候再去加載對(duì)應(yīng)的組件,那么用戶(hù)沒(méi)有必要在一開(kāi)始的時(shí)候就將所有的路由組件下載到本地。
在多頁(yè)面應(yīng)用中,我們往往可以將公共模塊進(jìn)行抽離,比如 header, footer 等等,這樣頁(yè)面在進(jìn)行跳轉(zhuǎn)的時(shí)候這些公共模塊因?yàn)榇嬖谟诰彺胬铮涂梢灾苯舆M(jìn)行加載了,而不是再進(jìn)行網(wǎng)絡(luò)請(qǐng)求了。
那么如何進(jìn)行拆包,分模塊進(jìn)行加載,這就需要 webpack 內(nèi)置插件:CommonsChunkPlugin,下面我將通過(guò)一個(gè)例子,來(lái)詮釋 webpack 該如何進(jìn)行配置。
本文的代碼放在我的 Github 上,有興趣的可以下載來(lái)看看:
git clone https://github.com/happylindz/blog.git cd blog/code/multiple-page-webpack-demo npm install
閱讀下面的內(nèi)容之前我強(qiáng)烈建議你看下我之前的文章:深入理解 webpack 文件打包機(jī)制,理解 webpack 文件的打包的機(jī)制有助于你更好地實(shí)現(xiàn)持久化緩存。
例子大概是這樣描述的:它由兩個(gè)頁(yè)面組成 pageA 和 pageB
// src/pageA.js import componentA from './common/componentA'; // 使用到 jquery 第三方庫(kù),需要抽離,避免業(yè)務(wù)打包文件過(guò)大 import $ from 'jquery'; // 加載 css 文件,一部分為公共樣式,一部分為獨(dú)有樣式,需要抽離 import './css/common.css' import './css/pageA.css'; console.log(componentA); console.log($.trim(' do something ')); // src/pageB.js // 頁(yè)面 A 和 B 都用到了公共模塊 componentA,需要抽離,避免重復(fù)加載 import componentA from './common/componentA'; import componentB from './common/componentB'; import './css/common.css' import './css/pageB.css'; console.log(componentA); console.log(componentB); // 用到異步加載模塊 asyncComponent,需要抽離,加載首屏速度 document.getElementById('xxxxx').addEventListener('click', () => { import( /* webpackChunkName: "async" */ './common/asyncComponent.js').then((async) => { async(); }) }) // 公共模塊基本長(zhǎng)這樣 export default "component X";
上面的頁(yè)面內(nèi)容基本簡(jiǎn)單涉及到了我們拆分模塊的三種模式:拆分公共庫(kù),按需加載和拆分公共模塊。那么接下來(lái)要來(lái)配置 webpack:
const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { entry: { pageA: [path.resolve(__dirname, './src/pageA.js')], pageB: path.resolve(__dirname, './src/pageB.js'), }, output: { path: path.resolve(__dirname, './dist'), filename: 'js/[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].js' }, module: { rules: [ { // 用正則去匹配要用該 loader 轉(zhuǎn)換的 CSS 文件 test: /.css$/, use: ExtractTextPlugin.extract({ fallback: "style-loader", use: ["css-loader"] }) } ] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'common', minChunks: 2, }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: ({ resource }) => ( resource && resource.indexOf('node_modules') >= 0 && resource.match(/.js$/) ) }), new ExtractTextPlugin({ filename: `css/[name].[chunkhash:8].css`, }), ] }
第一個(gè) CommonsChunkPlugin 用于抽離公共模塊,相當(dāng)于是說(shuō) webpack 大佬,如果你看到某個(gè)模塊被加載兩次即以上,那么請(qǐng)你幫我移到 common chunk 里面,這里 minChunks 為 2,粒度拆解最細(xì),你可以根據(jù)自己的實(shí)際情況,看選擇是用多少次模塊才將它們抽離。
第二個(gè) CommonsChunkPlugin 用來(lái)提取第三方代碼,將它們進(jìn)行抽離,判斷資源是否來(lái)自 node_modules,如果是,則說(shuō)明是第三方模塊,那就將它們抽離。相當(dāng)于是告訴 webpack 大佬,如果你看見(jiàn)某些模塊是來(lái)自 node_modules 目錄的,并且名字是 .js 結(jié)尾的話,麻煩把他們都移到 vendor chunk 里去,如果 vendor chunk 不存在的話,就創(chuàng)建一個(gè)新的。
這樣配置有什么好處,隨著業(yè)務(wù)的增長(zhǎng),我們依賴(lài)的第三方庫(kù)代碼很可能會(huì)越來(lái)越多,如果我們專(zhuān)門(mén)配置一個(gè)入口來(lái)存放第三方代碼,這時(shí)候我們的 webpack.config.js 就會(huì)變成:
// 不利于拓展 module.exports = { entry: { app: './src/main.js', vendor: [ 'vue', 'axio', 'vue-router', 'vuex', // more ], }, }
第三個(gè) ExtractTextPlugin 插件用于將 css 從打包好的 js 文件中抽離,生成獨(dú)立的 css 文件,想象一下,當(dāng)你只是修改了下樣式,并沒(méi)有修改頁(yè)面的功能邏輯,你肯定不希望你的 js 文件 hash 值變化,你肯定是希望 css 和 js 能夠相互分開(kāi),且互不影響。
運(yùn)行 webpack 后可以看到打包之后的效果:
├── css │ ├── common.2beb7387.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.2beb7387.js ├── pageA.d178426d.js ├── pageB.33931188.js └── vendor.22a1d956.js
可以看出 css 和 js 已經(jīng)分離,并且我們對(duì)模塊進(jìn)行了拆分,保證了模塊 chunk 的唯一性,當(dāng)你每次更新代碼的時(shí)候,會(huì)生成不一樣的 hash 值。
唯一性有了,那么我們需要保證 hash 值的穩(wěn)定性,試想下這樣的場(chǎng)景,你肯定不希望你修改某部分的代碼(模塊,css)導(dǎo)致了文件的 hash 值全變了,那么顯然是不明智的,那么我們?nèi)プ龅?hash 值變化最小化呢?
換句話說(shuō),我們就要找出 webpack 編譯中會(huì)導(dǎo)致緩存失效的因素,想辦法去解決或優(yōu)化它?
影響 chunkhash 值變化主要由以下四個(gè)部分引起的:
包含模塊的源代碼
webpack 用于啟動(dòng)運(yùn)行的 runtime 代碼
webpack 生成的模塊 moduleid(包括包含模塊 id 和被引用的依賴(lài)模塊 id)
chunkID
這四部分只要有任意部分發(fā)生變化,生成的分塊文件就不一樣了,緩存也就會(huì)失效,下面就從四個(gè)部分一一介紹:
一、源代碼變化:
顯然不用多說(shuō),緩存必須要刷新,不然就有問(wèn)題了
二、webpack 啟動(dòng)運(yùn)行的 runtime 代碼:
看過(guò)我之前的文章:深入理解 webpack 文件打包機(jī)制 就會(huì)知道,在 webpack 啟動(dòng)的時(shí)候需要執(zhí)行一些啟動(dòng)代碼。
(function(modules) { window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { // ... }; function __webpack_require__(moduleId) { // ... } __webpack_require__.e = function requireEnsure(chunkId, callback) { // ... script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js"; }; })([]);
大致內(nèi)容像上面這樣,它們是 webpack 的一些啟動(dòng)代碼,它們是一些函數(shù),告訴瀏覽器如何加載 webpack 定義的模塊。
其中有一行代碼每次更新都會(huì)改變的,因?yàn)閱?dòng)代碼需要清楚地知道 chunkid 和 chunkhash 值得對(duì)應(yīng)關(guān)系,這樣在異步加載的時(shí)候才能正確地拼接出異步 js 文件的路徑。
那么這部分代碼最終放在哪個(gè)文件呢?因?yàn)槲覀儎偛排渲玫臅r(shí)候最后生成的 common chunk 模塊,那么這部分運(yùn)行時(shí)代碼會(huì)被直接內(nèi)置在里面,這就導(dǎo)致了,我們每次更新我們業(yè)務(wù)代碼(pageA, pageB, 模塊)的時(shí)候, common chunkhash 會(huì)一直變化,但是這顯然不符合我們的設(shè)想,因?yàn)槲覀冎皇且?common chunk 用來(lái)存放公共模塊(這里指的是 componentA),那么我 componentA 都沒(méi)去修改,憑啥 chunkhash 需要變了。
所以我們需要將這部分 runtime 代碼抽離成單獨(dú)文件。
module.exports = { // ... plugins: [ // ... // 放到其他的 CommonsChunkPlugin 后面 new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, }), ] }
這相當(dāng)于是告訴 webpack 幫我把運(yùn)行時(shí)代碼抽離,放到單獨(dú)的文件中。
├── css │ ├── common.4cc08e4d.css │ ├── pageA.d178426d.css │ └── pageB.33931188.css └── js ├── async.03f28faf.js ├── common.4cc08e4d.js ├── pageA.d178426d.js ├── pageB.33931188.js ├── runtime.8c79fdcd.js └── vendor.cef44292.js
多生成了一個(gè) runtime.xxxx.js,以后你在改動(dòng)業(yè)務(wù)代碼的時(shí)候,common chunk 的 hash 值就不會(huì)變了,取而代之的是 runtime chunk hash 值會(huì)變,既然這部分代碼是動(dòng)態(tài)的,可以通過(guò) chunk-manifest-webpack-plugin 將他們 inline 到 html 中,減少一次網(wǎng)絡(luò)請(qǐng)求。
三、webpack 生成的模塊 moduleid
在 webpack2 中默認(rèn)加載 OccurrenceOrderPlugin 這個(gè)插件,OccurrenceOrderPlugin 插件會(huì)按引入次數(shù)最多的模塊進(jìn)行排序,引入次數(shù)的模塊的 moduleId 越小,但是這仍然是不穩(wěn)定的,隨著你代碼量的增加,雖然代碼引用次數(shù)的模塊 moduleId 越小,越不容易變化,但是難免還是不確定的。
默認(rèn)情況下,模塊的 id 是這個(gè)模塊在模塊數(shù)組中的索引。OccurenceOrderPlugin 會(huì)將引用次數(shù)多的模塊放在前面,在每次編譯時(shí)模塊的順序都是一致的,如果你修改代碼時(shí)新增或刪除了一些模塊,這將可能會(huì)影響到所有模塊的 id。
最佳實(shí)踐方案是通過(guò) HashedModuleIdsPlugin 這個(gè)插件,這個(gè)插件會(huì)根據(jù)模塊的相對(duì)路徑生成一個(gè)長(zhǎng)度只有四位的字符串作為模塊的 id,既隱藏了模塊的路徑信息,又減少了模塊 id 的長(zhǎng)度。
這樣一來(lái),改變 moduleId 的方式就只有文件路徑的改變了,只要你的文件路徑值不變,生成四位的字符串就不變,hash 值也不變。增加或刪除業(yè)務(wù)代碼模塊不會(huì)對(duì) moduleid 產(chǎn)生任何影響。
module.exports = { plugins: [ new webpack.HashedModuleIdsPlugin(), // 放在最前面 // ... ] }
四、chunkID
實(shí)際情況中分塊的個(gè)數(shù)的順序在多次編譯之間大多都是固定的, 不太容易發(fā)生變化。
這里涉及的只是比較基礎(chǔ)的模塊拆分,還有一些其它情況沒(méi)有考慮到,比如異步加載組件中包含公共模塊,可以再次將公共模塊進(jìn)行抽離。形成異步公共 chunk 模塊。有想深入學(xué)習(xí)的可以看這篇文章:Webpack 大法之 Code Splitting
webpack 做緩存的一些注意點(diǎn)
CSS 文件 hash 值失效的問(wèn)題
不建議線上發(fā)布使用 DllPlugin 插件
CSS 文件 hash 值失效的問(wèn)題:
ExtractTextPlugin 有個(gè)比較嚴(yán)重的問(wèn)題,那就是它生成文件名所用的[chunkhash]是直接取自于引用該 css 代碼段的 js chunk ;換句話說(shuō),如果我只是修改 css 代碼段,而不動(dòng) js 代碼,那么最后生成出來(lái)的 css 文件名依然沒(méi)有變化。
所以我們需要將 ExtractTextPlugin 中的 chunkhash 改為 contenthash,顧名思義,contenthash 代表的是文本文件內(nèi)容的 hash 值,也就是只有 style 文件的 hash 值。這樣編譯出來(lái)的 js 和 css 文件就有獨(dú)立的 hash 值了。
module.exports = { plugins: [ // ... new ExtractTextPlugin({ filename: `css/[name].[contenthash:8].css`, }), ] }
如果你使用的是 webpack2,webpack3,那么恭喜你,這樣就足夠了,js 文件和 css 文件修改都不會(huì)影響到相互的 hash 值。那如果你使用的是 webpack1,那么就會(huì)出現(xiàn)問(wèn)題。
具體來(lái)講就是 webpack1 和 webpack 在計(jì)算 chunkhash 值得不同:
webpack1 在涉及的時(shí)候并沒(méi)有考慮像 ExtractTextPlugin 會(huì)將模塊內(nèi)容抽離的問(wèn)題,所以它在計(jì)算 chunkhash 的時(shí)候是通過(guò)打包之前模塊內(nèi)容去計(jì)算的,也就是說(shuō)在計(jì)算的時(shí)候 css 內(nèi)容也包含在內(nèi),之后才將 css 內(nèi)容抽離成單獨(dú)的文件,
那么就會(huì)出現(xiàn):如果只修改了 css 文件,未修改引用的 js 文件,那么編譯輸出的 js 文件的 hash 值也會(huì)改變。
對(duì)此,webpack2 做了改進(jìn),它是基于打包后文件內(nèi)容來(lái)計(jì)算 hash 值的,所以是在 ExtractTextPlugin 抽離 css 代碼之后,所以就不存在上述這樣的問(wèn)題。如果不幸的你還在使用 webpack1,那么推薦你使用 md5-hash-webpack-plugin 插件來(lái)改變 webpack 計(jì)算 hash 的策略。
不建議線上發(fā)布使用 DllPlugin 插件
為什么這么說(shuō)呢?因?yàn)樽罱信笥褋?lái)問(wèn)我,他們 leader 不讓在線上用 DllPlugin 插件,來(lái)問(wèn)我為什么?
DllPlugin 本身有幾個(gè)缺點(diǎn):
首先你需要額外多配置一份 webpack 配置,增加工作量。
其中一個(gè)頁(yè)面用到了一個(gè)體積很大的第三方依賴(lài)庫(kù)而其它頁(yè)面根本不需要用到,但若直接將它打包在 dll.js 里很不值得,每次頁(yè)面打開(kāi)都要去加載這段無(wú)用的代碼,無(wú)法使用到 webpack2 的 Code Splitting 功能。
第一次打開(kāi)的時(shí)候需要下載 dll 文件,因?yàn)槟惆押芏鄮?kù)全部打在一起了,導(dǎo)致 dll 文件很大,首次進(jìn)入頁(yè)面加載速度很慢。
雖然你可以打包成 dll 文件,然后讓瀏覽器去讀取緩存,這樣下次就不用再去請(qǐng)求,比如你用 lodash 其中一個(gè)函數(shù),而你用dll會(huì)將整個(gè) lodash 文件打進(jìn)去,這就會(huì)導(dǎo)致你加載無(wú)用代碼過(guò)多,不利于首屏渲染時(shí)間。
我認(rèn)為的正確的姿勢(shì)是:
像 React、Vue 這樣整體性偏強(qiáng)的庫(kù),可以生成 vendor 第三方庫(kù)來(lái)去做緩存,因?yàn)槟阋话慵夹g(shù)體系是固定的,一個(gè)站點(diǎn)里面基本上都會(huì)用到統(tǒng)一技術(shù)體系,所以生成 vendor 庫(kù)用于緩存。
像 antd、lodash 這種功能性組件庫(kù),可以通過(guò) tree shaking 來(lái)進(jìn)行消除,只保留有用的代碼,千萬(wàn)不要直接打到 vendor 第三方庫(kù)里,不然你將大量執(zhí)行無(wú)用的代碼。
相信看了本文案例你已經(jīng)掌握了方法,更多精彩請(qǐng)關(guān)注Gxl網(wǎng)其它相關(guān)文章!
推薦閱讀:
JS數(shù)值類(lèi)型數(shù)組去重
項(xiàng)目中如何使用better-scroll插件
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com