Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 国产亚洲精品自在久久77,久久精品国产精品青草色艺,一级精品视频

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          你拆分JS代碼的方法可能是錯(cuò)的!

          者|David Gilbertson

          譯者|無(wú)明

          出處丨前端之巔

          一個(gè)網(wǎng)站該如何以最佳的方式向用戶發(fā)送資源文件?有很多不同的場(chǎng)景,不同的技術(shù)和不同的術(shù)語(yǔ)。在這篇文章里,我希望能夠讓你明白:哪種文件分割策略最適合你的網(wǎng)站和用戶,以及如何實(shí)現(xiàn)。

          根據(jù) Webpack 術(shù)語(yǔ)表,有兩種不同的文件分割類型。它們看起來(lái)似乎可以互換,但顯然不行:

          • 捆綁拆分:創(chuàng)建更多、更小的文件(但每個(gè)請(qǐng)求都需要加載它們)以獲得更好的緩存效果。
          • 代碼拆分:動(dòng)態(tài)加載代碼,用戶只下載他們正在查看的內(nèi)容所需的代碼。

          第二種方法看起來(lái)更有吸引力,不是嗎?事實(shí)上,有很多文章似乎都假設(shè)這是拆分 JavaScript 文件唯一有價(jià)值的方案。但我想要告訴你的是,對(duì)于很多網(wǎng)站來(lái)說(shuō),第一種方法更有價(jià)值,而且它應(yīng)該是你首先要考慮的。

          捆綁拆分

          捆綁拆分背后的想法非常簡(jiǎn)單。如果你有一個(gè)巨大的文件,哪怕只是修改了一行代碼,用戶也必須再次下載整個(gè)文件。但是,如果你將它分成兩個(gè)文件,那么用戶只需要下載被修改的那個(gè)文件,瀏覽器會(huì)從緩存中獲取另一個(gè)文件。

          捆綁拆分與緩存有關(guān),因此對(duì)于首次訪問(wèn)網(wǎng)站的用戶來(lái)說(shuō),有沒(méi)有拆分其實(shí)并沒(méi)有什么不同。

          對(duì)于頻繁訪問(wèn)網(wǎng)站的用戶來(lái)說(shuō),要衡量捆綁拆分所帶來(lái)的性能提升可能也很棘手,但我們必須這樣做!

          我需要一個(gè)表格來(lái)記錄性能數(shù)據(jù)。下面是上述提到的場(chǎng)景:

          • Alice 每周訪問(wèn)我們的網(wǎng)站一次,為期 10 周;
          • 我們每周更新一次網(wǎng)站;
          • 我們每周都會(huì)更新“產(chǎn)品列表”頁(yè)面;
          • 我們還有一個(gè)“產(chǎn)品詳細(xì)信息”頁(yè)面,目前還未開發(fā)出來(lái);
          • 在第 5 周,我們添加了一個(gè)新的 npm 包;
          • 在第 8 周,我們更新了一個(gè)現(xiàn)有的 npm 包。

          基 線

          假設(shè)我們的 JavaScript 包大小是 400 KB,只包含 main.js 單個(gè)文件。

          我們的 Webpack 配置如下(我省略了不相關(guān)的配置):

          const path = require('path');
          module.exports = {
           entry: path.resolve(__dirname, 'src/index.js'),
           output: {
           path: path.resolve(__dirname, 'dist'),
           filename: '[name].[contenthash].js',
           },
          };
          

          每個(gè)禮拜,當(dāng)我們做出一些變更時(shí),這個(gè)包的 contenthash 就會(huì)發(fā)生變化。因此,每周 Alice 訪問(wèn)我們的網(wǎng)站時(shí)必須下載新的 400 KB 文件。

          我們把這些數(shù)字記錄在表格中,它看起來(lái)就像這樣。

          下載量總共是 4.12 MB,為期 10 周。

          但我們可以做得更好。

          拆分 vendor 包

          現(xiàn)在,我們將包拆分為 main.js 和 vendor.js 文件。

          這很簡(jiǎn)單:

          const path = require('path');
          module.exports = {
           entry: path.resolve(__dirname, 'src/index.js'),
           output: {
           path: path.resolve(__dirname, 'dist'),
           filename: '[name].[contenthash].js',
           },
           optimization: {
           splitChunks: {
           chunks: 'all',
           },
           },
          };
          

          Webpack 4 努力為你做最好的事情,甚至都不需要告訴它你想要如何拆分捆綁包。

          有人說(shuō),“這樣看起來(lái)很整潔,不錯(cuò),Webpack!”

          也有人說(shuō),“你都對(duì)我的包做了什么?”

          設(shè)置 optimization.splitChunks.chunks ='all'意味著“將 node_modules 所有內(nèi)容都放入名為 vendors~main.js 的文件中”。

          經(jīng)過(guò)這個(gè)基本的捆綁拆分,Alice 每次訪問(wèn)網(wǎng)站時(shí)仍然需要下載 200 KB 的 main.js 新文件,然后分別在第 1 周,第 8 周和第 5 周下載 200 KB 的 vendor.js 文件。

          現(xiàn)在的下載量總共是 2.64 MB。

          減少了 36%。在配置中加了五行代碼,效果還不錯(cuò)。

          這樣的性能提升似乎有點(diǎn)微不足道,因?yàn)樗?10 周加起來(lái)的總和,但不管怎樣,向用戶發(fā)送的字節(jié)數(shù)確確實(shí)實(shí)減少了 36%,我們應(yīng)該為自己感到自豪。

          但我們可以做得更好。

          拆分每個(gè) npm 包

          vendors.js 遇到了與原來(lái) main.js 文件相同的問(wèn)題——對(duì)文件的一部分做出變更就必須重新下載整個(gè)文件。

          那么為什么不為每個(gè) npm 包提供單獨(dú)的文件呢?這很容易做到。

          所以讓我們將 react、lodash、redux 和 moment 等拆分成不同的文件:

          const path = require('path');
          const webpack = require('webpack');
          module.exports = {
           entry: path.resolve(__dirname, 'src/index.js'),
           plugins: [
           new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
           ],
           output: {
           path: path.resolve(__dirname, 'dist'),
           filename: '[name].[contenthash].js',
           },
           optimization: {
           runtimeChunk: 'single',
           splitChunks: {
           chunks: 'all',
           maxInitialRequests: Infinity,
           minSize: 0,
           cacheGroups: {
           vendor: {
           test: /[\\/]node_modules[\\/]/,
           name(module) {
           // get the name. E.g. node_modules/packageName/not/this/part.js
           // or node_modules/packageName
           const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
           // npm package names are URL-safe, but some servers don't like @ symbols
           return `npm.${packageName.replace('@', '')}`;
           },
           },
           },
           },
           },
          };
          

          Webpack 的文檔(https://webpack.js.org/guides/caching/)對(duì)此做出了很好的解釋,我會(huì)大致解釋一下 groovy 的部分,因?yàn)槲以谶@個(gè)上面花了很多時(shí)間:

          • Webpack 提供了一些不是那么聰明的默認(rèn)設(shè)置,比如分割輸出文件最多為 3 個(gè),最小文件的大小為 30 KB(更小的文件將被連接在一起),所以我覆蓋了這些設(shè)置。
          • 我們通過(guò) cacheGroups 來(lái)定義 Webpack 應(yīng)該如何將代碼塊分組到輸出文件中。在這里我使用了“vendor”,用于處理從 node_modules 加載的模塊。通常,你只需將輸出文件的 name 定義為字符串,但我將 name 定義為一個(gè)函數(shù)(在解析每個(gè)文件時(shí)調(diào)用這個(gè)函數(shù))。然后我基于模塊的路徑返回包的名稱。因此,對(duì)于每個(gè)包,我們都會(huì)得到一個(gè)文件,例如 npm.react-dom.899sadfhj4.js。
          • 出于發(fā)布的目的,NPM 包名稱必須是 URL 安全的(https://docs.npmjs.com/files/package.json#name), 因此我們不需要對(duì) packageName 進(jìn)行 encodeURI。但是,我遇到一個(gè)問(wèn)題,即.NET 服務(wù)器不支持帶有 @的文件名,所以我在這個(gè)代碼片段中將它替換掉。
          • 整個(gè)設(shè)置很棒,不需要額外的維護(hù)——我不需要引用任何包。

          Alice 每周仍然會(huì)重新下載 200 KB 的 main.js 文件,并且在她第一次訪問(wèn)網(wǎng)站時(shí)仍然會(huì)下載 200 KB 的 npm 軟件包,但她絕不會(huì)下載相同的軟件包兩次。

          現(xiàn)在的下載總量是 2.24 MB,與基線相比減少了 44%。

          我在想是否有可能減少 50%?

          拆分應(yīng)用程序代碼

          現(xiàn)在讓我們回到可憐的 Alice 一次又一次下載的 main.js 文件。

          我之前提到過(guò),我們的網(wǎng)站上有兩個(gè)截然不同的部分:產(chǎn)品列表頁(yè)面和產(chǎn)品詳細(xì)信息頁(yè)面。每個(gè)部分不一樣的代碼為 25 KB(共享代碼為 150 KB)。

          “產(chǎn)品詳細(xì)信息”頁(yè)面現(xiàn)在并沒(méi)有發(fā)生太大變化,因此,如果我們將其變?yōu)閱为?dú)的文件,大多數(shù)時(shí)候可以從緩存中獲取它。

          另外,我們有一個(gè)巨大的內(nèi)聯(lián) SVG 文件用于渲染圖標(biāo),大小為 25 KB,而且很少會(huì)發(fā)生改動(dòng)。

          我們應(yīng)該對(duì)此做些什么。

          我們手動(dòng)添加了一些條目,告訴 Webpack 為每一項(xiàng)創(chuàng)建一個(gè)文件。

          module.exports = {
           entry: {
           main: path.resolve(__dirname, 'src/index.js'),
           ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
           ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
           Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
           },
           output: {
           path: path.resolve(__dirname, 'dist'),
           filename: '[name].[contenthash:8].js',
           },
           plugins: [
           new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
           ],
           optimization: {
           runtimeChunk: 'single',
           splitChunks: {
           chunks: 'all',
           maxInitialRequests: Infinity,
           minSize: 0,
           cacheGroups: {
           vendor: {
           test: /[\\/]node_modules[\\/]/,
           name(module) {
           // get the name. E.g. node_modules/packageName/not/this/part.js
           // or node_modules/packageName
           const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
           // npm package names are URL-safe, but some servers don't like @ symbols
           return `npm.${packageName.replace('@', '')}`;
           },
           },
           },
           },
           },
          };
          

          Webpack 還會(huì)為 ProductList 和 ProductPage 之間共享的內(nèi)容創(chuàng)建文件,這樣我們就不會(huì)得到重復(fù)的代碼。

          這樣就可以為親愛(ài)的 Alice 節(jié)省 50 KB 的下載量。

          現(xiàn)在的總下載量只有 1.815 MB!

          我們已經(jīng)為 Alice 節(jié)省了高達(dá) 56%的下載量,在我們的理論場(chǎng)景中,這種情況可以一直持續(xù)下去。

          截止到目前,我們只是通過(guò)修改 Webpack 配置來(lái)實(shí)現(xiàn)這一切——我們沒(méi)有對(duì)應(yīng)用程序代碼進(jìn)行任何更改。

          我們的目標(biāo)是將應(yīng)用程序拆分為合理的小文件,讓用戶下載更少的代碼。

          因此,接下來(lái)我們要進(jìn)入代碼拆分,但首先我想要解決你現(xiàn)在想到的三個(gè)問(wèn)題。問(wèn)題 1:大量的網(wǎng)絡(luò)請(qǐng)求不是更慢嗎?

          對(duì)于這個(gè)問(wèn)題,答案是一個(gè)非常響亮的“不”。

          在 HTTP/1.1 時(shí)代或許是這種情況,但對(duì)于 HTTP/2 來(lái)說(shuō)并非如此。

          盡管一些著名的文章得出“即使使用 HTTP/2,下載太多文件仍然較慢”的結(jié)論,但在這些文章中,他們所謂的“太多”文件是指“數(shù)百個(gè)”文件。所以請(qǐng)記住,如果你有數(shù)百個(gè)文件,可能會(huì)達(dá)到并發(fā)上限。

          問(wèn)題 2:每個(gè) Webpack 捆綁包中不是有樣板代碼?

          是的。

          問(wèn)題 3:如果有多個(gè)小文件,不就失去了壓縮的優(yōu)勢(shì)了嗎?

          是的。

          好吧,這就是說(shuō):

          • 更多文件 = 更多的 Webpack 樣板代碼;
          • 更多文件 = 更少的壓縮。

          接下來(lái)讓我們做一下量化,這樣就可以確切地知道性能被磨損了多少。

          我做了一個(gè)測(cè)試,將 190KB 的文件拆分成 19 個(gè)小文件,這樣發(fā)送給瀏覽器的總字節(jié)數(shù)大約增加了 2%。

          在第一次訪問(wèn)時(shí)增加 2%,但在以后訪問(wèn)可以減少 60%,所以可以說(shuō)完全沒(méi)有磨損。

          我針對(duì) 1 個(gè)文件和 19 個(gè)文件分別進(jìn)行了測(cè)試,并基于不同的網(wǎng)絡(luò),包括 HTTP/1.1。

          這是結(jié)果表格,我想這足以說(shuō)明“更多文件會(huì)更好”:

          在 3G 和 4G 網(wǎng)絡(luò)上,當(dāng)有 19 個(gè)文件時(shí),總的加載時(shí)間縮短了 30%。

          當(dāng)然,這些數(shù)據(jù)帶有一定的噪音。例如,第二次在 4G 網(wǎng)絡(luò)上的加載時(shí)間為 646 毫秒,過(guò)了兩次之后需要 1116 毫秒——多了 73%。因此,聲稱 HTTP/2“快 30%”似乎有點(diǎn)心虛的感覺(jué)。

          我制作這張表來(lái)是想要量化 HTTP/2 的差異,但看來(lái)我唯一能說(shuō)的是“它可能沒(méi)有顯著差異”。

          真正的驚喜是最后兩行,我原本認(rèn)為舊的 Windows 和 HTTP/1.1 會(huì)很慢。

          這就是我要說(shuō)的有關(guān)捆綁拆分的一切。我認(rèn)為這種方法的唯一缺點(diǎn)是要不斷地說(shuō)服人們,加載大量小文件是沒(méi)有問(wèn)題的。

          現(xiàn)在,讓我們談?wù)劻硪环N類型的文件拆分。

          代碼拆分(不加載不需要的代碼)

          這種方法可能只對(duì)某些網(wǎng)站有用。

          我發(fā)明了 20/20 規(guī)則:如果你的網(wǎng)站的某些部分只有 20%的用戶訪問(wèn),而這部分超過(guò)了整個(gè)網(wǎng)站 20%的 JavaScript,那么你應(yīng)該按需加載這些代碼。

          顯然,因?yàn)榇嬖诟鼜?fù)雜的場(chǎng)景,所以這個(gè)數(shù)字顯然需要做出調(diào)整。但關(guān)鍵在于,肯定存在一個(gè)平衡點(diǎn),到了這個(gè)平衡點(diǎn),代碼拆分對(duì)于你的網(wǎng)站來(lái)說(shuō)可能就沒(méi)有意義了。

          如何找到這個(gè)平衡點(diǎn)?

          假設(shè)你有一個(gè)購(gòu)物網(wǎng)站,你想知道是否應(yīng)該對(duì)“結(jié)帳”代碼進(jìn)行拆分,因?yàn)橹挥?30%的用戶會(huì)進(jìn)行這個(gè)操作。

          你需要弄清楚有多少代碼是只與結(jié)賬這個(gè)功能有關(guān)的。因?yàn)樵谶M(jìn)行“代碼拆分”之前已經(jīng)進(jìn)行了“捆綁拆分”,因此你可能已經(jīng)知道這部分究竟有多少代碼。

          只與結(jié)帳有關(guān)的代碼是 7 KB,其余部分是 300 KB。看到這個(gè)我會(huì)說(shuō),我不會(huì)想去拆分這個(gè)代碼,原因如下:

          • 預(yù)先加載它并不慢,因?yàn)槟闶遣⑿屑虞d這些文件的。而且你可以試試是否有可能記錄 300 KB 和 307 KB 加載時(shí)間的差異。
          • 如果你稍后加載這段代碼,用戶在點(diǎn)擊“Take My Money”后將不得不等待加載這個(gè)文件——這是你最不想遇到摩擦阻力的時(shí)候。
          • 進(jìn)行代碼拆分需要更改應(yīng)用程序代碼。它會(huì)在以前只有同步邏輯的地方引入異步邏輯。這不是火箭科學(xué),但它的復(fù)雜性,我認(rèn)為應(yīng)該通過(guò)對(duì)用戶體驗(yàn)可感知的改進(jìn)來(lái)證明。。

          現(xiàn)在讓我們來(lái)看看兩個(gè)需要代碼拆分的例子。

          polyfill

          我之所以從這里開始講起,是因?yàn)樗m用于大多數(shù)網(wǎng)站,而且介紹起來(lái)相對(duì)簡(jiǎn)單。

          我在網(wǎng)站上使用了很多花哨的功能,有一個(gè)文件導(dǎo)入了所有需要的 polyfill。其中包括以下八行:

          require('whatwg-fetch');
          require('intl');
          require('url-polyfill');
          require('core-js/web/dom-collections');
          require('core-js/es6/map');
          require('core-js/es6/string');
          require('core-js/es6/array');
          require('core-js/es6/object');
          

          我在 index.js 的頂部導(dǎo)入了這個(gè)文件。

          import './polyfills';
          import React from 'react';
          import ReactDOM from 'react-dom';
          import App from './App/App';
          import './index.css';
          const render = () => {
           ReactDOM.render(<App />, document.getElementById('root'));
          }
          render(); // yes I am pointless, for now
          

          根據(jù)之前的捆綁拆分的 Webpack 配置,polyfill 將自動(dòng)被拆分為四個(gè)不同的文件,因?yàn)檫@里有四個(gè) npm 包。它們總共約 25 KB,但 90%的瀏覽器都不需要它們,所以有必要進(jìn)行動(dòng)態(tài)加載。

          使用 Webpack 4 和 import() 語(yǔ)法(不要與 import 語(yǔ)法混淆)可以很方便地實(shí)現(xiàn) polyfill 的條件加載。

          import React from 'react';
          import ReactDOM from 'react-dom';
          import App from './App/App';
          import './index.css';
          const render = () => {
           ReactDOM.render(<App />, document.getElementById('root'));
          }
          if (
           'fetch' in window &&
           'Intl' in window &&
           'URL' in window &&
           'Map' in window &&
           'forEach' in NodeList.prototype &&
           'startsWith' in String.prototype &&
           'endsWith' in String.prototype &&
           'includes' in String.prototype &&
           'includes' in Array.prototype &&
           'assign' in Object &&
           'entries' in Object &&
           'keys' in Object
          ) {
           render();
          } else {
           import('./polyfills').then(render);
          }
          

          如果瀏覽器支持所有功能,那么就渲染頁(yè)面,否則的話就導(dǎo)入 polyfill,然后渲染頁(yè)面。在瀏覽器中運(yùn)行這些代碼時(shí),Webpack 的運(yùn)行時(shí)將負(fù)責(zé)加載這四個(gè) npm 包,在下載和解析它們之后,將調(diào)用 render()……

          順便說(shuō)一句,要使用 import(),需要 Babel 的 dynamic-import 插件 (https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/)。 另外,正如 Webpack 文檔解釋的那樣,import() 使用了 promise,所以你需要單獨(dú)對(duì)其進(jìn)行 polyfill。

          這個(gè)很簡(jiǎn)單,對(duì)嗎?下面來(lái)點(diǎn)稍微有難度的。

          基于路由的動(dòng)態(tài)加載(特定于 React)

          回到 Alice 的例子,我們假設(shè)網(wǎng)站有一個(gè)“管理”功能,賣家可以登錄并管理他們的商品。

          這部分有很多精彩的功能,大量的圖表,需要很多 npm 大圖表庫(kù)。因?yàn)橐呀?jīng)在進(jìn)行了捆綁拆分,所以它們都是 100 KB 左右的文件。

          目前,我的路由設(shè)置是當(dāng)用戶訪問(wèn) /admin 時(shí),將會(huì)渲染<AdminPage>。當(dāng) Webpack 將所有內(nèi)容捆綁在一起時(shí),它會(huì)找到 import AdminPage from ./AdminPage.js,然后說(shuō),“我需要將它包含在初始化代碼中”。

          但我們不希望它這樣。我們需要使用動(dòng)態(tài)導(dǎo)入,例如 import(‘/AdminPage.js’),這樣 Webpack 就知道要進(jìn)行動(dòng)態(tài)加載了。

          這很酷,不需要做任何配置。

          因此,我可以創(chuàng)建另一個(gè)組件,當(dāng)用戶訪問(wèn) /admin 時(shí)就會(huì)渲染這個(gè)組件,而不是直接引用 AdminPage。它看起來(lái)可能像這樣:

          import React from 'react';
          class AdminPageLoader extends React.PureComponent {
           constructor(props) {
           super(props);
           this.state = {
           AdminPage: null,
           }
           }
           componentDidMount() {
           import('./AdminPage').then(module => {
           this.setState({ AdminPage: module.default });
           });
           }
           render() {
           const { AdminPage } = this.state;
           return AdminPage
           ? <AdminPage {...this.props} />
           : <div>Loading...</div>;
           }
          }
          export default AdminPageLoader;
          

          這個(gè)概念很簡(jiǎn)單。在加載這個(gè)組件時(shí)(意味著用戶在訪問(wèn) /admin),我們將動(dòng)態(tài)加載./AdminPage.js,然后在 state 中保存對(duì)該組件的引用。

          在等待<AdminPage>加載時(shí),我們只是在 render() 方法中渲染<div> Loading... </div>,或者在加載完成時(shí)渲染<AdminPage>,并保存在 state 中。

          我自己這樣做是為了好玩,但在現(xiàn)實(shí)世界中,你可以使用 react-loadable,正如 React 文檔(https://reactjs.org/docs/code-splitting.html) 中關(guān)于代碼拆分的描述那樣。

          以上就是所有我想說(shuō)的話,簡(jiǎn)單地說(shuō)就是:

          如果用戶會(huì)多次訪問(wèn)你的網(wǎng)站,請(qǐng)將你的代碼拆分為很多小文件。如果你的網(wǎng)站有些部分是大部分用戶不會(huì)訪問(wèn)到的,請(qǐng)動(dòng)態(tài)加載這些代碼。

          英文原文

          https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758

          質(zhì)文章,第一時(shí)間送達(dá)!

          學(xué)習(xí)全文大概需要 12分鐘,內(nèi)容實(shí)戰(zhàn)性較強(qiáng)。

          1. 前言

          本篇將基于Python 3.7+Django 3.0結(jié)合Vue.js前端框架,為大家介紹如何基于這三者的技術(shù)棧來(lái)實(shí)現(xiàn)一個(gè)前端后離的Web開發(fā)項(xiàng)目。為了簡(jiǎn)化,方便讀者理解,本文將以開發(fā)一個(gè)單體頁(yè)面應(yīng)用作為實(shí)戰(zhàn)演示。

          2. 先搞清楚什么是前后端分離

          在正式開始實(shí)戰(zhàn)示例之前,我們有必要先弄清楚一個(gè)概念:什么是前后端分離?

          前后端分離目前已成為互聯(lián)網(wǎng)項(xiàng)目開發(fā)的業(yè)界標(biāo)準(zhǔn)使用方式,在聊前后端分離之前,相信也有很多讀者,對(duì)如何區(qū)分前端還是后端,還搞不清楚(是不是讓我戳中了你的痛處了)。本著“致良知”,先科譜一下知識(shí)。

          通常情況下,我們說(shuō)的前端,大多是指瀏覽器這一端,一般是用Html+CSS+JS來(lái)實(shí)現(xiàn)的,所以通常會(huì)引申為用Html+CSS+JS寫的大部分程序都是前端,包括App,小程序,H5等。

          PS: 在NodeJS出現(xiàn)之后,用NodeJS寫的后端部分,也會(huì)被人歸類為前端,為了區(qū)分之前的前端,就給他們起了一個(gè)名字,叫做“大前端”。

          久而久之,人們習(xí)慣把Html+CSS+JS,運(yùn)行在瀏覽器端執(zhí)行的,稱之為前端。

          而Java,C,Python,PHP這些運(yùn)行在服務(wù)器端的,統(tǒng)一稱之為后端。

          但,這種以語(yǔ)言為分界點(diǎn)去區(qū)分前后端,真的合理么?顯然不合理!

          前后端的定義,不應(yīng)該是以語(yǔ)言來(lái)定義,而是應(yīng)該以它的運(yùn)行環(huán)境,如果是在服務(wù)器端,就應(yīng)該被稱之為后端,代表著你看不見(jiàn),摸不著。而如果運(yùn)行在用戶端,就應(yīng)該被稱之為前端,代表你是可以看得到的。

          在不分前后端的時(shí)候,無(wú)論是Java還是JS,全都是一個(gè)人來(lái)寫。

          為什么現(xiàn)在很多互聯(lián)網(wǎng)公司在項(xiàng)目開發(fā)時(shí),建議要進(jìn)行前后端分離,或者說(shuō)前后端分離能帶來(lái)哪些優(yōu)勢(shì)?(好處多多,這里僅提兩個(gè)點(diǎn))

          • 第一個(gè),并行開發(fā)、獨(dú)立部署、實(shí)現(xiàn)前后端解,前后端的進(jìn)度互不影響,在過(guò)去,前后端不分離的情況下,項(xiàng)目代碼耦合嚴(yán)重相互影響,且前后端人員工作量分布不均。

          • 第二個(gè),術(shù)業(yè)有專攻(開發(fā)人員分離),以前的JavaWeb項(xiàng)目大多數(shù)都是Java程序員又當(dāng)?shù)之?dāng)媽,又搞前端,又搞后端。前后端分離之后,前端工程師只管前端的事情,后端工程師只管后端的事情。

          我們先看看一個(gè) Web 系統(tǒng),在前后端不分離時(shí)架構(gòu)設(shè)計(jì)是什么樣的。

          用戶在瀏覽器上發(fā)送請(qǐng)求,服務(wù)器端接收到請(qǐng)求,根據(jù) Header 中的 token 進(jìn)行用戶鑒權(quán),從數(shù)據(jù)庫(kù)取出數(shù)據(jù),處理后將結(jié)果數(shù)據(jù)填入 HTML 模板,返回給瀏覽器,瀏覽器將 HTML 展現(xiàn)給用戶。

          而采用前后端分離之后,分離的是人員職責(zé),人員職責(zé)分離了,因此架構(gòu)也發(fā)生變化。

          前后端分離后,前端人員和后端人員約定好接口,前端人員不用再關(guān)心業(yè)務(wù)處理是怎么回事,他只需要把界面做好就可以了,后端人員也不用再關(guān)系前端界面是什么樣的,他只需要做好業(yè)務(wù)邏輯處理即可。

          小結(jié)一下,前后端分離是什么?

          前后端分離是一種架構(gòu)模式,或者說(shuō)是最佳實(shí)踐,它主張將前端開發(fā)人員和后端開發(fā)人員的工作進(jìn)行解耦,盡量減少他她們之間的交流成本,幫助他她們更能專注于自己擅長(zhǎng)的工作。

          PS: 本篇實(shí)戰(zhàn)示例,使用Vue.js作為前端框架,代替Django本身自帶的模板引擎,Django則作為服務(wù)端提供API接口,從而實(shí)現(xiàn)前后端分離。

          3. 環(huán)境準(zhǔn)備

          本實(shí)戰(zhàn)示例,基礎(chǔ)環(huán)境對(duì)應(yīng)安裝版本如下:

          • Python 3.7.4

          • Mysql 5.7

          • Pycharm (建議專業(yè)版)

          • Node

          PS: 其中Python、Mysql、Pycharm、Node安裝過(guò)程皆較為簡(jiǎn)單,不是本文介紹重點(diǎn),讀者可直接參考官網(wǎng)安裝方法。

          4. 新建獨(dú)立的虛擬開發(fā)環(huán)境

          1、創(chuàng)建一個(gè)用于Django項(xiàng)目開發(fā)的獨(dú)立虛擬環(huán)境,切換到本地開發(fā)目錄,輸入如下命令:

          python3 -m venv venv

          2、創(chuàng)建完成后,目錄結(jié)構(gòu)如下:

          ? venv tree -L 2.├── bin│ ├── activate│ ├── activate.csh│ ├── activate.fish│ ├── easy_install│ ├── easy_install-3.7│ ├── pip│ ├── pip3│ ├── pip3.7│ ├── python -> python3│ └── python3 -> /usr/local/bin/python3├── include├── lib│ └── python3.7└── pyvenv.cfg
          4 directories, 11 files

          3、進(jìn)入到bin目錄,輸入命令source activate 命令,激活虛擬環(huán)境。

          4、虛擬環(huán)境激活后,如上圖所示。接下來(lái),在虛擬環(huán)境安裝Django庫(kù)。

          安裝Django (最新版本為3.0)

          (venv) ? pip install Django

          Django 項(xiàng)目源碼:

          https://github.com/django/django

          Django3.0 版本特性可查閱官網(wǎng):

          https://docs.djangoproject.com/en/3.0/releases/3.0/

          5、安裝完成后,可檢查一下版本信息:

          (venv) ? pythonPython 3.7.4 (default, Jul 9 2019, 18:15:00)[Clang 10.0.0 (clang-1000.11.45.5)] on darwinType "help", "copyright", "credits" or "license" for more information.>>> import django>>> print(django.get_version)3.0

          可以發(fā)現(xiàn),在虛擬環(huán)境中已經(jīng)成功安裝好了Django 3.0。

          5. 創(chuàng)建Django后端項(xiàng)目

          1、創(chuàng)建Django項(xiàng)目,采用Pycharm或者命令行創(chuàng)建皆可。此處,以命令行方式作為演示,項(xiàng)目名為django_vue。

          (venv) ? django-admin startproject django_vue

          2. Django項(xiàng)目創(chuàng)建完成后,目錄結(jié)構(gòu)如下所示。

          ├── django_vue│ ├── django_vue│ │ ├── __init__.py│ │ ├── asgi.py│ │ ├── settings.py│ │ ├── urls.py│ │ └── wsgi.py│ └── manage.py

          3、執(zhí)行同步數(shù)據(jù)庫(kù)文件(Django默認(rèn)數(shù)據(jù)庫(kù)為db.sqlite3),執(zhí)行同步過(guò)程如下:

          (venv) ? python manage.py migrateOperations to perform: Apply all migrations: admin, auth, contenttypes, sessionsRunning migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying sessions.0001_initial... OK

          4、啟動(dòng)Django Server ,驗(yàn)證默認(rèn)配置是否正常。

          (venv) ? python manage.py runserver 0.0.0.0:8000Watching for file changes with StatReloaderPerforming system checks...
          System check identified no issues (0 silenced).December 15, 2019 - 08:36:28Django version 3.0, using settings 'django_vue.settings'Starting development server at http://0.0.0.0:8000/Quit the server with CONTROL-C.

          5、打開瀏覽器,訪問(wèn)http://localhost:8000,一切正常的話,可見(jiàn)到如下界面。

          6. 將Django數(shù)據(jù)庫(kù)更換為Mysql

          1、假設(shè)在前面,我們已經(jīng)安裝配置好了Mysql,輸入如下命令進(jìn)入到Mysql。

          mysql -u root -p

          2、創(chuàng)建數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)取名為django_vue_db,并設(shè)置字符集為utf-8。

          mysql> CREATE DATABASE django_vue_db CHARACTER SET utf8;Query OK, 1 row affected (0.01 sec)

          3、安裝myslqclient庫(kù)

          (venv) ? pip install mysqlclient

          4、配置settings.py文件,配置Mysql數(shù)據(jù)庫(kù)引擎。

          ```python
          DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'django_vue_db', 'USER': 'root', 'PASSWORD': 'xxxxxxx', 'HOST': '127.0.0.1', }}```

          5、執(zhí)行同步操作,將數(shù)據(jù)遷移到Mysql。

          python manage.py migrate

          6、驗(yàn)證是否切庫(kù)成功,進(jìn)入到Mysql客戶端,查看django初化表是否有生成。

          mysql> use django_vue_db;Database changedmysql> show tables;+----------------------------+| Tables_in_django_vue_db |+----------------------------+| auth_group || auth_group_permissions || auth_permission || auth_user || auth_user_groups || auth_user_user_permissions || django_admin_log || django_content_type || django_migrations || django_session |+----------------------------+10 rows in set (0.00 sec)

          7、運(yùn)行Django Server,重新訪問(wèn)http://localhost:8000。

          python manage.py runserver 0.0.0.0:8000

          如果能正常訪問(wèn),過(guò)程沒(méi)有報(bào)錯(cuò),說(shuō)明切換數(shù)據(jù)庫(kù)已經(jīng)成功了。

          7. 創(chuàng)建Django實(shí)戰(zhàn)項(xiàng)目App

          1、創(chuàng)建Django App,進(jìn)入django_vue項(xiàng)目主目錄,輸入如下命令:

          (venv) ? python manage.py startapp api_test

          2、App創(chuàng)建完成后,目錄結(jié)構(gòu)如下所示:

          ├── api_test│ ├── __init__.py│ ├── admin.py│ ├── apps.py│ ├── migrations│ │ └── __init__.py│ ├── models.py│ ├── tests.py│ └── views.py

          并把a(bǔ)pi_test加入到settings文件中的installed_apps列表里:

          ```pythonINSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'api_test',]```

          3、 在api_test目錄下的models.py里我們簡(jiǎn)單寫一個(gè)model如下:

          ```python# -*- coding: utf-8 -*-from __future__ import unicode_literalsfrom django.db import modelsclass Book(models.Model): book_name = models.CharField(max_length=128) add_time = models.DateTimeField(auto_now_add=True)
          def __unicode__(self): return self.book_name```

          只有兩個(gè)字段,書名book_name和添加時(shí)間add_time。如果沒(méi)有指定主鍵的話Django會(huì)自動(dòng)新增一個(gè)自增id作為主鍵。

          4、在api_test目錄下的views里我們新增兩個(gè)接口,一個(gè)是show_books返回所有的書籍列表(通過(guò)JsonResponse返回能被前端識(shí)別的json格式數(shù)據(jù)),二是add_book接受一個(gè)get請(qǐng)求,往數(shù)據(jù)庫(kù)里添加一條book數(shù)據(jù)。

          ```pythonfrom django.shortcuts import renderfrom django.views.decorators.http import require_http_methodsfrom django.core import serializersfrom django.http import JsonResponseimport json
          from .models import Book
          @require_http_methods(["GET"])def add_book(request): response = {} try: book = Book(book_name=request.GET.get('book_name')) book.save response['msg'] = 'success' response['error_num'] = 0 except Exception as e: response['msg'] = str(e) response['error_num'] = 1 return JsonResponse(response)
          @require_http_methods(["GET"])def show_books(request): response = {} try: books = Book.objects.filter response['list'] = json.loads(serializers.serialize("json", books)) response['msg'] = 'success' response['error_num'] = 0 except Exception as e: response['msg'] = str(e) response['error_num'] = 1 return JsonResponse(response)```

          可以看出,在ORM的幫忙下,我們的接口實(shí)際上不需要自己去組織SQL代碼。

          5、在api_test目錄下,新增一個(gè)urls.py文件,把我們新增的兩個(gè)接口添加到路由里:

          from django.conf.urls import url, includefrom .views import *
          urlpatterns = [ url(r"add_book$", add_book, ), url(r"show_books$", show_books, ),]```

          6、我們還要把a(bǔ)pi_test下的urls添加到項(xiàng)目django_vue下的urls中,才能完成路由:

          ```pythonfrom django.contrib import adminfrom django.urls import pathfrom django.conf.urls import url, includefrom django.contrib import adminfrom django.views.generic import TemplateViewimport api_test.urls
          urlpatterns = [ url(r"^admin/", admin.site.urls), url(r'^api/', include(api_test.urls)),]```

          7、在項(xiàng)目的根目錄,輸入命令:

          python manage.py makemigrations api_testpython manage.py migrate

          8、查詢數(shù)據(jù)庫(kù),看到book表已經(jīng)自動(dòng)創(chuàng)建了:

          mysql> show tables;+----------------------------+| Tables_in_django_vue_db |+----------------------------+| api_test_book || auth_group || auth_group_permissions || auth_permission || auth_user || auth_user_groups || auth_user_user_permissions || django_admin_log || django_content_type || django_migrations || django_session |+----------------------------+11 rows in set (0.00 sec)mysql> desc api_test_book;+-----------+--------------+------+-----+---------+----------------+| Field | Type | | Key | Default | Extra |+-----------+--------------+------+-----+---------+----------------+| id | int(11) | NO | PRI | | auto_increment || book_name | varchar(128) | NO | | | || add_time | datetime(6) | NO | | | |+-----------+--------------+------+-----+---------+----------------+3 rows in set (0.01 sec)mysql>```

          Django生成的表名將以app名加上model中的類名組合而成。

          9、在項(xiàng)目的根目錄,輸入命令:

          python manage.py runserver 0.0.0.0:800

          啟動(dòng)服務(wù),通過(guò)httpie測(cè)試一下我們剛才寫的兩個(gè)接口。

          10、通過(guò)調(diào)用接口向Django App中添加兩條書名記錄。

          ```shell? http http://127.0.0.1:8000/api/add_book\?book_name\=mikezhou_talkHTTP/1.1 200 OKContent-Length: 34Content-Type: application/jsonDate: Sun, 15 Dec 2019 09:11:12 GMTServer: WSGIServer/0.2 CPython/3.7.4X-Content-Type-Options: nosniffX-Frame-Options: DENY{ "error_num": 0, "msg": "success"}

          ? http http://127.0.0.1:8000/api/add_book\?book_name\=測(cè)試開發(fā)技術(shù)HTTP/1.1 200 OKContent-Length: 34Content-Type: application/jsonDate: Sun, 15 Dec 2019 09:11:44 GMTServer: WSGIServer/0.2 CPython/3.7.4X-Content-Type-Options: nosniffX-Frame-Options: DENY{ "error_num": 0, "msg": "success"}```

          11、通過(guò)調(diào)用接口,顯示Django App中所有書名列表:

          ```shell? http http://127.0.0.1:8000/api/show_booksHTTP/1.1 200 OKContent-Length: 305Content-Type: application/jsonDate: Sun, 15 Dec 2019 09:13:48 GMTServer: WSGIServer/0.2 CPython/3.7.4X-Content-Type-Options: nosniffX-Frame-Options: DENY{ "error_num": 0, "list": [ { "fields": { "add_time": "2019-12-15T09:11:12.673Z", "book_name": "mikezhou_talk" }, "model": "api_test.book", "pk": 1 }, { "fields": { "add_time": "2019-12-15T09:11:44.305Z", "book_name": "測(cè)試開發(fā)技術(shù)" }, "model": "api_test.book", "pk": 2 } ], "msg": "success"}```

          8. 新建Vue.js前端項(xiàng)目

          1、有關(guān)Vue的模塊(包括vue)可以使用node自帶的npm包管理器安裝。推薦使用淘寶的 cnpm 命令行工具代替默認(rèn)的 npm。

          npm install -g cnpm --registry=https://registry.npm.taobao.org

          2、先用cnpm安裝vue-cli腳手架工具(vue-cli是官方腳手架工具,能迅速幫你搭建起vue項(xiàng)目的框架):

          cnpm install -g vue-cli

          3、安裝好后,在django_vue項(xiàng)目根目錄下,新建一個(gè)前端工程目錄:

          vue-init webpack frontend

          在創(chuàng)建項(xiàng)目的過(guò)程中會(huì)彈出一些與項(xiàng)目相關(guān)的選項(xiàng)需要回答,按照真實(shí)情況進(jìn)行輸入即可。

          4、安裝 vue 依賴模塊

          cd frontendcnpm installcnpm install vue-resourcecnpm install element-ui

          5、現(xiàn)在我們可以看到整個(gè)文件目錄結(jié)構(gòu)是這樣的:

          本文為了讀者方便查看,是直接將vue前端工程放在django項(xiàng)目目錄下,實(shí)際多人協(xié)作開發(fā)過(guò)程中,完全是可以放在不同代碼倉(cāng)庫(kù)下面的。

          6、在frontend目錄src下包含入口文件main.js,入口組件App.vue等。后綴為vue的文件是Vue.js框架定義的單文件組件,其中標(biāo)簽中的內(nèi)容可以理解為是類html的頁(yè)面結(jié)構(gòu)內(nèi)容。

          7、在src/component文件夾下新建一個(gè)名為Home.vue的組件,通過(guò)調(diào)用之前在Django上寫好的api,實(shí)現(xiàn)添加書籍和展示書籍信息的功能。在樣式組件上我們使用了餓了么團(tuán)隊(duì)推出的element-ui,這是一套專門匹配Vue.js框架的功能樣式組件。由于組件的編碼涉及到了很多js、html、css的知識(shí),并不是本文的重點(diǎn),因此在此只貼出部分代碼:

          Home.vue文件代碼:

          ```vue
          <template><div class="home"><el-row display="margin-top:10px"><el-input v-model="input" placeholder="請(qǐng)輸入書名" style="display:inline-table; width: 30%; float:left"></el-input><el-button type="primary" @click="addBook" style="float:left; margin: 2px;">新增</el-button></el-row><el-row><el-table :data="bookList" style="width: 100%" border><el-table-column prop="id" label="編號(hào)" min-width="100"><template slot-scope="scope"> {{ scope.row.pk }} </template></el-table-column><el-table-column prop="book_name" label="書名" min-width="100"><template slot-scope="scope"> {{ scope.row.fields.book_name }} </template></el-table-column><el-table-column prop="add_time" label="添加時(shí)間" min-width="100"><template slot-scope="scope"> {{ scope.row.fields.add_time }} </template></el-table-column></el-table> </el-row> </div></template>
          <script>export default { name: 'home', data { return { input: '', bookList: } }, mounted: function { this.showBooks }, methods: { addBook { this.$http.get('http://127.0.0.1:8000/api/add_book?book_name=' + this.input) .then((response) => { var res = JSON.parse(response.bodyText) if (res.error_num === 0) { this.showBooks } else { this.$message.error('新增書籍失敗,請(qǐng)重試') console.log(res['msg']) } }) }, showBooks { this.$http.get('http://127.0.0.1:8000/api/show_books') .then((response) => { var res = JSON.parse(response.bodyText) console.log(res) if (res.error_num === 0) { this.bookList = res['list'] } else { this.$message.error('查詢書籍失敗') console.log(res['msg']) } }) } }}</script>
          <!-- Add "scoped" attribute to limit CSS to this component only --><style scoped> h1, h2 { font-weight: normal; }
          ul { list-style-type: none; padding: 0;}
          li { display: inline-block; margin: 0 10px;}
          a { color: #42b983;}</style>```

          8、在src/router目錄的index.js中,我們把新建的Home組件,配置到vue-router路由中:

          ```jsimport Vue from 'vue'import Router from 'vue-router'// import HelloWorld from '@/components/HelloWorld'import Home from '@/components/Home'Vue.use(Router)export default new Router({ routes: [ { path: '/', name: 'Home', component: Home } ]})```

          9、在src/main.js文件中,導(dǎo)入element-ui、vue-resource庫(kù)。

          ```jsimport Vue from 'vue'import App from './App'import router from './router'import ElementUI from 'element-ui'import VueResource from 'vue-resource'import 'element-ui/lib/theme-chalk/index.css'Vue.use(ElementUI)Vue.use(VueResource)Vue.config.productionTip = false
          /* eslint-disable no-new */new Vue({ el: '#app', router, components: { App }, template: '<App/>'})```

          10、如果出現(xiàn)跨域問(wèn)題,需要在Django層注入header,用Django的第三方包django-cors-headers來(lái)解決跨域問(wèn)題:

          pip install django-cors-headers

          settings.py 修改:

          ```pythonMIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',]
          CORS_ORIGIN_ALLOW_ALL = True```

          PS: 注意中間件的添加順序。

          12、在前端工程frontend目錄下,輸入npm run dev啟動(dòng)node自帶的服務(wù)器,瀏覽器會(huì)自動(dòng)打開, 我們能看到頁(yè)面:

          13、嘗試新增書籍,如填入:“自動(dòng)化測(cè)試實(shí)戰(zhàn)寶典”,新增的書籍信息會(huì)實(shí)時(shí)反映到頁(yè)面的列表中,這得益于Vue.js的數(shù)據(jù)雙向綁定特性。

          14、在前端工程frontend目錄下,輸入npm run build,如果項(xiàng)目沒(méi)有錯(cuò)誤的話,就能夠看到所有的組件、css、圖片等都被webpack自動(dòng)打包到dist目錄下了:

          9. 整合Django和Vue.js前端

          目前我們已經(jīng)分別完成了Django后端和Vue.js前端工程的創(chuàng)建和編寫,但實(shí)際上它們是運(yùn)行在各自的服務(wù)器上,和我們的要求是不一致的。因此我們須要把Django的TemplateView指向我們剛才生成的前端dist文件即可。

          1、 找到project目錄的urls.py,使用通用視圖創(chuàng)建最簡(jiǎn)單的模板控制器,訪問(wèn) 『/』時(shí)直接返回 index.html:

          ```pythonurlpatterns = [ url(r"^admin/", admin.site.urls), url(r'^api/', include(api_test.urls)), url(r'^$', TemplateView.as_view(template_name="index.html")),]```

          2、上一步使用了Django的模板系統(tǒng),所以需要配置一下模板使Django知道從哪里找到index.html。在project目錄的settings.py下:

          ```pythonTEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS':['frontend/dist'], 'APP_DIRS':True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, },]```

          3、 我們還需要配置一下靜態(tài)文件的搜索路徑。同樣是project目錄的settings.py下:

          ```python# Add for vuejsSTATICFILES_DIRS = [ os.path.join(BASE_DIR, "frontend/dist/static"),]```

          4、 配置完成,我們?cè)趐roject目錄下輸入命令python manage.py runserver,就能夠看到我們的前端頁(yè)面在瀏覽器上展現(xiàn):

          注意此時(shí)服務(wù)的端口已經(jīng)是Django服務(wù)的8000而不是node服務(wù)的8080了,說(shuō)明我們已經(jīng)成功通過(guò)Django集成了Vue前端工程。

          該實(shí)戰(zhàn)示例為大家充分展示了現(xiàn)在主流的前后端分離方式,由前端框架,如Vue.js來(lái)構(gòu)建實(shí)現(xiàn)前端界面,再通過(guò)后端框架,如Django來(lái)實(shí)現(xiàn)API數(shù)據(jù)提供,兩者通過(guò)接口進(jìn)行通訊、相輔相成、最終實(shí)現(xiàn)一個(gè)完整Web項(xiàng)目。

          聲明:前后端分離圖片來(lái)源于網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系刪除。

          個(gè)網(wǎng)站該如何以最佳的方式向用戶發(fā)送資源文件?有很多不同的場(chǎng)景,不同的技術(shù)和不同的術(shù)語(yǔ)。在這篇文章里,我希望能夠讓你明白:哪種文件分割策略最適合你的網(wǎng)站和用戶,以及如何實(shí)現(xiàn)。

          根據(jù) Webpack 術(shù)語(yǔ)表,有兩種不同的文件分割類型。它們看起來(lái)似乎可以互換,但顯然不行:

          • 捆綁拆分:創(chuàng)建更多、更小的文件(但每個(gè)請(qǐng)求都需要加載它們)以獲得更好的緩存效果。
          • 代碼拆分:動(dòng)態(tài)加載代碼,用戶只下載他們正在查看的內(nèi)容所需的代碼。

          第二種方法看起來(lái)更有吸引力,不是嗎?事實(shí)上,有很多文章似乎都假設(shè)這是拆分 JavaScript 文件唯一有價(jià)值的方案。但我想要告訴你的是,對(duì)于很多網(wǎng)站來(lái)說(shuō),第一種方法更有價(jià)值,而且它應(yīng)該是你首先要考慮的。

          捆綁拆分

          捆綁拆分背后的想法非常簡(jiǎn)單。如果你有一個(gè)巨大的文件,哪怕只是修改了一行代碼,用戶也必須再次下載整個(gè)文件。但是,如果你將它分成兩個(gè)文件,那么用戶只需要下載被修改的那個(gè)文件,瀏覽器會(huì)從緩存中獲取另一個(gè)文件。

          捆綁拆分與緩存有關(guān),因此對(duì)于首次訪問(wèn)網(wǎng)站的用戶來(lái)說(shuō),有沒(méi)有拆分其實(shí)并沒(méi)有什么不同。

          對(duì)于頻繁訪問(wèn)網(wǎng)站的用戶來(lái)說(shuō),要衡量捆綁拆分所帶來(lái)的性能提升可能也很棘手,但我們必須這樣做!

          我需要一個(gè)表格來(lái)記錄性能數(shù)據(jù)。下面是上述提到的場(chǎng)景:

          • Alice 每周訪問(wèn)我們的網(wǎng)站一次,為期 10 周;
          • 我們每周更新一次網(wǎng)站;
          • 我們每周都會(huì)更新“產(chǎn)品列表”頁(yè)面;
          • 我們還有一個(gè)“產(chǎn)品詳細(xì)信息”頁(yè)面,目前還未開發(fā)出來(lái);
          • 在第 5 周,我們添加了一個(gè)新的 npm 包;
          • 在第 8 周,我們更新了一個(gè)現(xiàn)有的 npm 包。

          基 線

          假設(shè)我們的 JavaScript 包大小是 400 KB,只包含 main.js 單個(gè)文件。

          我們的 Webpack 配置如下(我省略了不相關(guān)的配置):

           const path = require('path');
           ?
           module.exports = {
             entry: path.resolve(__dirname, 'src/index.js'),
             output: {
               path: path.resolve(__dirname, 'dist'),
               filename: '[name].[contenthash].js',
             },
           };

          每個(gè)禮拜,當(dāng)我們做出一些變更時(shí),這個(gè)包的 contenthash 就會(huì)發(fā)生變化。因此,每周 Alice 訪問(wèn)我們的網(wǎng)站時(shí)必須下載新的 400 KB 文件。

          我們把這些數(shù)字記錄在表格中,它看起來(lái)就像這樣。

          下載量總共是 4.12 MB,為期 10 周。

          但我們可以做得更好。

          拆分 vendor 包

          現(xiàn)在,我們將包拆分為 main.js 和 vendor.js 文件。

          這很簡(jiǎn)單:

           const path = require('path');
           ?
           module.exports = {
             entry: path.resolve(__dirname, 'src/index.js'),
             output: {
               path: path.resolve(__dirname, 'dist'),
               filename: '[name].[contenthash].js',
             },
             optimization: {
               splitChunks: {
                 chunks: 'all',
               },
             },
           };

          Webpack 4 努力為你做最好的事情,甚至都不需要告訴它你想要如何拆分捆綁包。

          有人說(shuō),“這樣看起來(lái)很整潔,不錯(cuò),Webpack!”

          也有人說(shuō),“你都對(duì)我的包做了什么?”

          設(shè)置 optimization.splitChunks.chunks ='all'意味著“將 node_modules 所有內(nèi)容都放入名為 vendors~main.js 的文件中”。

          經(jīng)過(guò)這個(gè)基本的捆綁拆分,Alice 每次訪問(wèn)網(wǎng)站時(shí)仍然需要下載 200 KB 的 main.js 新文件,然后分別在第 1 周,第 8 周和第 5 周下載 200 KB 的 vendor.js 文件。

          現(xiàn)在的下載量總共是 2.64 MB。

          減少了 36%。在配置中加了五行代碼,效果還不錯(cuò)。

          這樣的性能提升似乎有點(diǎn)微不足道,因?yàn)樗?10 周加起來(lái)的總和,但不管怎樣,向用戶發(fā)送的字節(jié)數(shù)確確實(shí)實(shí)減少了 36%,我們應(yīng)該為自己感到自豪。

          但我們可以做得更好。

          拆分每個(gè) npm 包

          vendors.js 遇到了與原來(lái) main.js 文件相同的問(wèn)題——對(duì)文件的一部分做出變更就必須重新下載整個(gè)文件。

          那么為什么不為每個(gè) npm 包提供單獨(dú)的文件呢?這很容易做到。

          所以讓我們將 react、lodash、redux 和 moment 等拆分成不同的文件:

           const path = require('path');
           const webpack = require('webpack');
           ?
           module.exports = {
             entry: path.resolve(__dirname, 'src/index.js'),
             plugins: [
               new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
             ],
             output: {
               path: path.resolve(__dirname, 'dist'),
               filename: '[name].[contenthash].js',
             },
             optimization: {
               runtimeChunk: 'single',
               splitChunks: {
                 chunks: 'all',
                 maxInitialRequests: Infinity,
                 minSize: 0,
                 cacheGroups: {
                   vendor: {
                     test: /[\\/]node_modules[\\/]/,
                     name(module) {
                       // get the name. E.g. node_modules/packageName/not/this/part.js
                       // or node_modules/packageName
                       const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
           ?
                       // npm package names are URL-safe, but some servers don't like @ symbols
                       return `npm.${packageName.replace('@', '')}`;
                     },
                   },
                 },
               },
             },
           };

          Webpack 的文檔(https://webpack.js.org/guides/caching/)對(duì)此做出了很好的解釋,我會(huì)大致解釋一下 groovy 的部分,因?yàn)槲以谶@個(gè)上面花了很多時(shí)間:

          • Webpack 提供了一些不是那么聰明的默認(rèn)設(shè)置,比如分割輸出文件最多為 3 個(gè),最小文件的大小為 30 KB(更小的文件將被連接在一起),所以我覆蓋了這些設(shè)置。
          • 我們通過(guò) cacheGroups 來(lái)定義 Webpack 應(yīng)該如何將代碼塊分組到輸出文件中。在這里我使用了“vendor”,用于處理從 node_modules 加載的模塊。通常,你只需將輸出文件的 name 定義為字符串,但我將 name 定義為一個(gè)函數(shù)(在解析每個(gè)文件時(shí)調(diào)用這個(gè)函數(shù))。然后我基于模塊的路徑返回包的名稱。因此,對(duì)于每個(gè)包,我們都會(huì)得到一個(gè)文件,例如 npm.react-dom.899sadfhj4.js。
          • 出于發(fā)布的目的,NPM 包名稱必須是 URL 安全的(https://docs.npmjs.com/files/package.json#name), 因此我們不需要對(duì) packageName 進(jìn)行 encodeURI。但是,我遇到一個(gè)問(wèn)題,即.NET 服務(wù)器不支持帶有 @的文件名,所以我在這個(gè)代碼片段中將它替換掉。
          • 整個(gè)設(shè)置很棒,不需要額外的維護(hù)——我不需要引用任何包。

          Alice 每周仍然會(huì)重新下載 200 KB 的 main.js 文件,并且在她第一次訪問(wèn)網(wǎng)站時(shí)仍然會(huì)下載 200 KB 的 npm 軟件包,但她絕不會(huì)下載相同的軟件包兩次。

          現(xiàn)在的下載總量是 2.24 MB,與基線相比減少了 44%。

          我在想是否有可能減少 50%?

          拆分應(yīng)用程序代碼

          現(xiàn)在讓我們回到可憐的 Alice 一次又一次下載的 main.js 文件。

          我之前提到過(guò),我們的網(wǎng)站上有兩個(gè)截然不同的部分:產(chǎn)品列表頁(yè)面和產(chǎn)品詳細(xì)信息頁(yè)面。每個(gè)部分不一樣的代碼為 25 KB(共享代碼為 150 KB)。

          “產(chǎn)品詳細(xì)信息”頁(yè)面現(xiàn)在并沒(méi)有發(fā)生太大變化,因此,如果我們將其變?yōu)閱为?dú)的文件,大多數(shù)時(shí)候可以從緩存中獲取它。

          另外,我們有一個(gè)巨大的內(nèi)聯(lián) SVG 文件用于渲染圖標(biāo),大小為 25 KB,而且很少會(huì)發(fā)生改動(dòng)。

          我們應(yīng)該對(duì)此做些什么。

          我們手動(dòng)添加了一些條目,告訴 Webpack 為每一項(xiàng)創(chuàng)建一個(gè)文件。

           module.exports = {
             entry: {
               main: path.resolve(__dirname, 'src/index.js'),
               ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
               ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
               Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
             },
             output: {
               path: path.resolve(__dirname, 'dist'),
               filename: '[name].[contenthash:8].js',
             },
             plugins: [
               new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
             ],
             optimization: {
               runtimeChunk: 'single',
               splitChunks: {
                 chunks: 'all',
                 maxInitialRequests: Infinity,
                 minSize: 0,
                 cacheGroups: {
                   vendor: {
                     test: /[\\/]node_modules[\\/]/,
                     name(module) {
                       // get the name. E.g. node_modules/packageName/not/this/part.js
                       // or node_modules/packageName
                       const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
           ?
                       // npm package names are URL-safe, but some servers don't like @ symbols
                       return `npm.${packageName.replace('@', '')}`;
                     },
                   },
                 },
               },
             },
           };

          Webpack 還會(huì)為 ProductList 和 ProductPage 之間共享的內(nèi)容創(chuàng)建文件,這樣我們就不會(huì)得到重復(fù)的代碼。

          這樣就可以為親愛(ài)的 Alice 節(jié)省 50 KB 的下載量。

          現(xiàn)在的總下載量只有 1.815 MB!

          我們已經(jīng)為 Alice 節(jié)省了高達(dá) 56%的下載量,在我們的理論場(chǎng)景中,這種情況可以一直持續(xù)下去。

          截止到目前,我們只是通過(guò)修改 Webpack 配置來(lái)實(shí)現(xiàn)這一切——我們沒(méi)有對(duì)應(yīng)用程序代碼進(jìn)行任何更改。

          我們的目標(biāo)是將應(yīng)用程序拆分為合理的小文件,讓用戶下載更少的代碼。

          因此,接下來(lái)我們要進(jìn)入代碼拆分,但首先我想要解決你現(xiàn)在想到的三個(gè)問(wèn)題。問(wèn)題 1:大量的網(wǎng)絡(luò)請(qǐng)求不是更慢嗎?

          對(duì)于這個(gè)問(wèn)題,答案是一個(gè)非常響亮的“不”。

          在 HTTP/1.1 時(shí)代或許是這種情況,但對(duì)于 HTTP/2 來(lái)說(shuō)并非如此。

          盡管一些著名的文章得出“即使使用 HTTP/2,下載太多文件仍然較慢”的結(jié)論,但在這些文章中,他們所謂的“太多”文件是指“數(shù)百個(gè)”文件。所以請(qǐng)記住,如果你有數(shù)百個(gè)文件,可能會(huì)達(dá)到并發(fā)上限。

          問(wèn)題 2:每個(gè) Webpack 捆綁包中不是有樣板代碼?

          是的。

          問(wèn)題 3:如果有多個(gè)小文件,不就失去了壓縮的優(yōu)勢(shì)了嗎?

          是的。

          好吧,這就是說(shuō):

          • 更多文件 = 更多的 Webpack 樣板代碼;
          • 更多文件 = 更少的壓縮。

          接下來(lái)讓我們做一下量化,這樣就可以確切地知道性能被磨損了多少。

          我做了一個(gè)測(cè)試,將 190KB 的文件拆分成 19 個(gè)小文件,這樣發(fā)送給瀏覽器的總字節(jié)數(shù)大約增加了 2%。

          在第一次訪問(wèn)時(shí)增加 2%,但在以后訪問(wèn)可以減少 60%,所以可以說(shuō)完全沒(méi)有磨損。

          我針對(duì) 1 個(gè)文件和 19 個(gè)文件分別進(jìn)行了測(cè)試,并基于不同的網(wǎng)絡(luò),包括 HTTP/1.1。

          這是結(jié)果表格,我想這足以說(shuō)明“更多文件會(huì)更好”:

          在 3G 和 4G 網(wǎng)絡(luò)上,當(dāng)有 19 個(gè)文件時(shí),總的加載時(shí)間縮短了 30%。

          當(dāng)然,這些數(shù)據(jù)帶有一定的噪音。例如,第二次在 4G 網(wǎng)絡(luò)上的加載時(shí)間為 646 毫秒,過(guò)了兩次之后需要 1116 毫秒——多了 73%。因此,聲稱 HTTP/2“快 30%”似乎有點(diǎn)心虛的感覺(jué)。

          我制作這張表來(lái)是想要量化 HTTP/2 的差異,但看來(lái)我唯一能說(shuō)的是“它可能沒(méi)有顯著差異”。

          真正的驚喜是最后兩行,我原本認(rèn)為舊的 Windows 和 HTTP/1.1 會(huì)很慢。

          這就是我要說(shuō)的有關(guān)捆綁拆分的一切。我認(rèn)為這種方法的唯一缺點(diǎn)是要不斷地說(shuō)服人們,加載大量小文件是沒(méi)有問(wèn)題的。

          現(xiàn)在,讓我們談?wù)劻硪环N類型的文件拆分。

          代碼拆分(不加載不需要的代碼)

          這種方法可能只對(duì)某些網(wǎng)站有用。

          我發(fā)明了 20/20 規(guī)則:如果你的網(wǎng)站的某些部分只有 20%的用戶訪問(wèn),而這部分超過(guò)了整個(gè)網(wǎng)站 20%的 JavaScript,那么你應(yīng)該按需加載這些代碼。

          顯然,因?yàn)榇嬖诟鼜?fù)雜的場(chǎng)景,所以這個(gè)數(shù)字顯然需要做出調(diào)整。但關(guān)鍵在于,肯定存在一個(gè)平衡點(diǎn),到了這個(gè)平衡點(diǎn),代碼拆分對(duì)于你的網(wǎng)站來(lái)說(shuō)可能就沒(méi)有意義了。

          如何找到這個(gè)平衡點(diǎn)?

          假設(shè)你有一個(gè)購(gòu)物網(wǎng)站,你想知道是否應(yīng)該對(duì)“結(jié)帳”代碼進(jìn)行拆分,因?yàn)橹挥?30%的用戶會(huì)進(jìn)行這個(gè)操作。

          你需要弄清楚有多少代碼是只與結(jié)賬這個(gè)功能有關(guān)的。因?yàn)樵谶M(jìn)行“代碼拆分”之前已經(jīng)進(jìn)行了“捆綁拆分”,因此你可能已經(jīng)知道這部分究竟有多少代碼。

          只與結(jié)帳有關(guān)的代碼是 7 KB,其余部分是 300 KB。看到這個(gè)我會(huì)說(shuō),我不會(huì)想去拆分這個(gè)代碼,原因如下:

          • 預(yù)先加載它并不慢,因?yàn)槟闶遣⑿屑虞d這些文件的。而且你可以試試是否有可能記錄 300 KB 和 307 KB 加載時(shí)間的差異。
          • 如果你稍后加載這段代碼,用戶在點(diǎn)擊“Take My Money”后將不得不等待加載這個(gè)文件——這是你最不想遇到摩擦阻力的時(shí)候。
          • 進(jìn)行代碼拆分需要更改應(yīng)用程序代碼。它會(huì)在以前只有同步邏輯的地方引入異步邏輯。這不是火箭科學(xué),但它的復(fù)雜性,我認(rèn)為應(yīng)該通過(guò)對(duì)用戶體驗(yàn)可感知的改進(jìn)來(lái)證明。。

          現(xiàn)在讓我們來(lái)看看兩個(gè)需要代碼拆分的例子。

          polyfill

          我之所以從這里開始講起,是因?yàn)樗m用于大多數(shù)網(wǎng)站,而且介紹起來(lái)相對(duì)簡(jiǎn)單。

          我在網(wǎng)站上使用了很多花哨的功能,有一個(gè)文件導(dǎo)入了所有需要的 polyfill。其中包括以下八行:

           require('whatwg-fetch');
           require('intl');
           require('url-polyfill');
           require('core-js/web/dom-collections');
           require('core-js/es6/map');
           require('core-js/es6/string');
           require('core-js/es6/array');
           require('core-js/es6/object');

          我在 index.js 的頂部導(dǎo)入了這個(gè)文件。

           import './polyfills';
           import React from 'react';
           import ReactDOM from 'react-dom';
           import App from './App/App';
           import './index.css';
           ?
           const render = () => {
             ReactDOM.render(<App />, document.getElementById('root'));
           }
           ?
           render(); // yes I am pointless, for now

          根據(jù)之前的捆綁拆分的 Webpack 配置,polyfill 將自動(dòng)被拆分為四個(gè)不同的文件,因?yàn)檫@里有四個(gè) npm 包。它們總共約 25 KB,但 90%的瀏覽器都不需要它們,所以有必要進(jìn)行動(dòng)態(tài)加載。

          使用 Webpack 4 和 import() 語(yǔ)法(不要與 import 語(yǔ)法混淆)可以很方便地實(shí)現(xiàn) polyfill 的條件加載。

           import React from 'react';
           import ReactDOM from 'react-dom';
           import App from './App/App';
           import './index.css';
           ?
           const render = () => {
             ReactDOM.render(<App />, document.getElementById('root'));
           }
           ?
           if (
             'fetch' in window &&
             'Intl' in window &&
             'URL' in window &&
             'Map' in window &&
             'forEach' in NodeList.prototype &&
             'startsWith' in String.prototype &&
             'endsWith' in String.prototype &&
             'includes' in String.prototype &&
             'includes' in Array.prototype &&
             'assign' in Object &&
             'entries' in Object &&
             'keys' in Object
           ) {
             render();
           } else {
             import('./polyfills').then(render);
           }

          如果瀏覽器支持所有功能,那么就渲染頁(yè)面,否則的話就導(dǎo)入 polyfill,然后渲染頁(yè)面。在瀏覽器中運(yùn)行這些代碼時(shí),Webpack 的運(yùn)行時(shí)將負(fù)責(zé)加載這四個(gè) npm 包,在下載和解析它們之后,將調(diào)用 render()……

          順便說(shuō)一句,要使用 import(),需要 Babel 的 dynamic-import 插件 (https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/)。 另外,正如 Webpack 文檔解釋的那樣,import() 使用了 promise,所以你需要單獨(dú)對(duì)其進(jìn)行 polyfill。

          這個(gè)很簡(jiǎn)單,對(duì)嗎?下面來(lái)點(diǎn)稍微有難度的。

          基于路由的動(dòng)態(tài)加載(特定于 React)

          回到 Alice 的例子,我們假設(shè)網(wǎng)站有一個(gè)“管理”功能,賣家可以登錄并管理他們的商品。

          這部分有很多精彩的功能,大量的圖表,需要很多 npm 大圖表庫(kù)。因?yàn)橐呀?jīng)在進(jìn)行了捆綁拆分,所以它們都是 100 KB 左右的文件。

          目前,我的路由設(shè)置是當(dāng)用戶訪問(wèn) /admin 時(shí),將會(huì)渲染<AdminPage>。當(dāng) Webpack 將所有內(nèi)容捆綁在一起時(shí),它會(huì)找到 import AdminPage from ./AdminPage.js,然后說(shuō),“我需要將它包含在初始化代碼中”。

          但我們不希望它這樣。我們需要使用動(dòng)態(tài)導(dǎo)入,例如 import(‘/AdminPage.js’),這樣 Webpack 就知道要進(jìn)行動(dòng)態(tài)加載了。

          這很酷,不需要做任何配置。

          因此,我可以創(chuàng)建另一個(gè)組件,當(dāng)用戶訪問(wèn) /admin 時(shí)就會(huì)渲染這個(gè)組件,而不是直接引用 AdminPage。它看起來(lái)可能像這樣:

           import React from 'react';
           ?
           class AdminPageLoader extends React.PureComponent {
             constructor(props) {
               super(props);
           ?
               this.state = {
                 AdminPage: null,
               }
             }
           ?
             componentDidMount() {
               import('./AdminPage').then(module => {
                 this.setState({ AdminPage: module.default });
               });
             }
           ?
             render() {
               const { AdminPage } = this.state;
           ?
               return AdminPage
                 ? <AdminPage {...this.props} />
                 : <div>Loading...</div>;
             }
           }
           ?
           export default AdminPageLoader;

          這個(gè)概念很簡(jiǎn)單。在加載這個(gè)組件時(shí)(意味著用戶在訪問(wèn) /admin),我們將動(dòng)態(tài)加載./AdminPage.js,然后在 state 中保存對(duì)該組件的引用。

          在等待<AdminPage>加載時(shí),我們只是在 render() 方法中渲染<div> Loading... </div>,或者在加載完成時(shí)渲染<AdminPage>,并保存在 state 中。

          我自己這樣做是為了好玩,但在現(xiàn)實(shí)世界中,你可以使用 react-loadable,正如 React 文檔(https://reactjs.org/docs/code-splitting.html) 中關(guān)于代碼拆分的描述那樣。

          以上就是所有我想說(shuō)的話,簡(jiǎn)單地說(shuō)就是:

          如果用戶會(huì)多次訪問(wèn)你的網(wǎng)站,請(qǐng)將你的代碼拆分為很多小文件。如果你的網(wǎng)站有些部分是大部分用戶不會(huì)訪問(wèn)到的,請(qǐng)動(dòng)態(tài)加載這些代碼。


          主站蜘蛛池模板: 少妇无码一区二区三区| 亚洲国产美国国产综合一区二区 | 国产成人精品亚洲一区| 红桃AV一区二区三区在线无码AV| 国产一区二区久久久| 国产成人AV一区二区三区无码| 一级特黄性色生活片一区二区| 亚洲中文字幕乱码一区| 麻豆高清免费国产一区| 无码一区二区三区在线观看| 中文字幕VA一区二区三区| 午夜福利国产一区二区| 日韩福利视频一区| 日韩视频一区二区| 欧洲精品一区二区三区| 精品国产一区二区三区AV | 国产一区二区三区樱花动漫| 精品国产一区AV天美传媒| 亚洲无线码一区二区三区| 人妻少妇精品一区二区三区| 不卡无码人妻一区三区音频| 久久精品无码一区二区三区日韩| 无码日韩人妻AV一区免费l| 亚洲国产高清在线一区二区三区 | 久久久综合亚洲色一区二区三区| 精品日韩在线视频一区二区三区 | 岛国无码av不卡一区二区| 国产韩国精品一区二区三区| 国产精久久一区二区三区| 中文字幕不卡一区| 无码人妻精品一区二区在线视频 | 日本精品一区二区久久久| 国产伦精品一区二区三区视频小说| 四虎在线观看一区二区| 在线观看国产一区亚洲bd| 日韩精品一区二区三区不卡| 97精品国产福利一区二区三区| 日韩精品成人一区二区三区| 久久精品无码一区二区WWW| 无码人妻aⅴ一区二区三区有奶水| 国产一区内射最近更新|