者|David Gilbertson
譯者|無明
出處丨前端之巔
一個網站該如何以最佳的方式向用戶發送資源文件?有很多不同的場景,不同的技術和不同的術語。在這篇文章里,我希望能夠讓你明白:哪種文件分割策略最適合你的網站和用戶,以及如何實現。
根據 Webpack 術語表,有兩種不同的文件分割類型。它們看起來似乎可以互換,但顯然不行:
第二種方法看起來更有吸引力,不是嗎?事實上,有很多文章似乎都假設這是拆分 JavaScript 文件唯一有價值的方案。但我想要告訴你的是,對于很多網站來說,第一種方法更有價值,而且它應該是你首先要考慮的。
捆綁拆分背后的想法非常簡單。如果你有一個巨大的文件,哪怕只是修改了一行代碼,用戶也必須再次下載整個文件。但是,如果你將它分成兩個文件,那么用戶只需要下載被修改的那個文件,瀏覽器會從緩存中獲取另一個文件。
捆綁拆分與緩存有關,因此對于首次訪問網站的用戶來說,有沒有拆分其實并沒有什么不同。
對于頻繁訪問網站的用戶來說,要衡量捆綁拆分所帶來的性能提升可能也很棘手,但我們必須這樣做!
我需要一個表格來記錄性能數據。下面是上述提到的場景:
假設我們的 JavaScript 包大小是 400 KB,只包含 main.js 單個文件。
我們的 Webpack 配置如下(我省略了不相關的配置):
const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, };
每個禮拜,當我們做出一些變更時,這個包的 contenthash 就會發生變化。因此,每周 Alice 訪問我們的網站時必須下載新的 400 KB 文件。
我們把這些數字記錄在表格中,它看起來就像這樣。
下載量總共是 4.12 MB,為期 10 周。
但我們可以做得更好。
拆分 vendor 包
現在,我們將包拆分為 main.js 和 vendor.js 文件。
這很簡單:
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 努力為你做最好的事情,甚至都不需要告訴它你想要如何拆分捆綁包。
有人說,“這樣看起來很整潔,不錯,Webpack!”
也有人說,“你都對我的包做了什么?”
設置 optimization.splitChunks.chunks ='all'意味著“將 node_modules 所有內容都放入名為 vendors~main.js 的文件中”。
經過這個基本的捆綁拆分,Alice 每次訪問網站時仍然需要下載 200 KB 的 main.js 新文件,然后分別在第 1 周,第 8 周和第 5 周下載 200 KB 的 vendor.js 文件。
現在的下載量總共是 2.64 MB。
減少了 36%。在配置中加了五行代碼,效果還不錯。
這樣的性能提升似乎有點微不足道,因為它是 10 周加起來的總和,但不管怎樣,向用戶發送的字節數確確實實減少了 36%,我們應該為自己感到自豪。
但我們可以做得更好。
vendors.js 遇到了與原來 main.js 文件相同的問題——對文件的一部分做出變更就必須重新下載整個文件。
那么為什么不為每個 npm 包提供單獨的文件呢?這很容易做到。
所以讓我們將 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/)對此做出了很好的解釋,我會大致解釋一下 groovy 的部分,因為我在這個上面花了很多時間:
Alice 每周仍然會重新下載 200 KB 的 main.js 文件,并且在她第一次訪問網站時仍然會下載 200 KB 的 npm 軟件包,但她絕不會下載相同的軟件包兩次。
現在的下載總量是 2.24 MB,與基線相比減少了 44%。
我在想是否有可能減少 50%?
現在讓我們回到可憐的 Alice 一次又一次下載的 main.js 文件。
我之前提到過,我們的網站上有兩個截然不同的部分:產品列表頁面和產品詳細信息頁面。每個部分不一樣的代碼為 25 KB(共享代碼為 150 KB)。
“產品詳細信息”頁面現在并沒有發生太大變化,因此,如果我們將其變為單獨的文件,大多數時候可以從緩存中獲取它。
另外,我們有一個巨大的內聯 SVG 文件用于渲染圖標,大小為 25 KB,而且很少會發生改動。
我們應該對此做些什么。
我們手動添加了一些條目,告訴 Webpack 為每一項創建一個文件。
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 還會為 ProductList 和 ProductPage 之間共享的內容創建文件,這樣我們就不會得到重復的代碼。
這樣就可以為親愛的 Alice 節省 50 KB 的下載量。
現在的總下載量只有 1.815 MB!
我們已經為 Alice 節省了高達 56%的下載量,在我們的理論場景中,這種情況可以一直持續下去。
截止到目前,我們只是通過修改 Webpack 配置來實現這一切——我們沒有對應用程序代碼進行任何更改。
我們的目標是將應用程序拆分為合理的小文件,讓用戶下載更少的代碼。
因此,接下來我們要進入代碼拆分,但首先我想要解決你現在想到的三個問題。問題 1:大量的網絡請求不是更慢嗎?
對于這個問題,答案是一個非常響亮的“不”。
在 HTTP/1.1 時代或許是這種情況,但對于 HTTP/2 來說并非如此。
盡管一些著名的文章得出“即使使用 HTTP/2,下載太多文件仍然較慢”的結論,但在這些文章中,他們所謂的“太多”文件是指“數百個”文件。所以請記住,如果你有數百個文件,可能會達到并發上限。
問題 2:每個 Webpack 捆綁包中不是有樣板代碼?
是的。
問題 3:如果有多個小文件,不就失去了壓縮的優勢了嗎?
是的。
好吧,這就是說:
接下來讓我們做一下量化,這樣就可以確切地知道性能被磨損了多少。
我做了一個測試,將 190KB 的文件拆分成 19 個小文件,這樣發送給瀏覽器的總字節數大約增加了 2%。
在第一次訪問時增加 2%,但在以后訪問可以減少 60%,所以可以說完全沒有磨損。
我針對 1 個文件和 19 個文件分別進行了測試,并基于不同的網絡,包括 HTTP/1.1。
這是結果表格,我想這足以說明“更多文件會更好”:
在 3G 和 4G 網絡上,當有 19 個文件時,總的加載時間縮短了 30%。
當然,這些數據帶有一定的噪音。例如,第二次在 4G 網絡上的加載時間為 646 毫秒,過了兩次之后需要 1116 毫秒——多了 73%。因此,聲稱 HTTP/2“快 30%”似乎有點心虛的感覺。
我制作這張表來是想要量化 HTTP/2 的差異,但看來我唯一能說的是“它可能沒有顯著差異”。
真正的驚喜是最后兩行,我原本認為舊的 Windows 和 HTTP/1.1 會很慢。
這就是我要說的有關捆綁拆分的一切。我認為這種方法的唯一缺點是要不斷地說服人們,加載大量小文件是沒有問題的。
現在,讓我們談談另一種類型的文件拆分。
這種方法可能只對某些網站有用。
我發明了 20/20 規則:如果你的網站的某些部分只有 20%的用戶訪問,而這部分超過了整個網站 20%的 JavaScript,那么你應該按需加載這些代碼。
顯然,因為存在更復雜的場景,所以這個數字顯然需要做出調整。但關鍵在于,肯定存在一個平衡點,到了這個平衡點,代碼拆分對于你的網站來說可能就沒有意義了。
如何找到這個平衡點?
假設你有一個購物網站,你想知道是否應該對“結帳”代碼進行拆分,因為只有 30%的用戶會進行這個操作。
你需要弄清楚有多少代碼是只與結賬這個功能有關的。因為在進行“代碼拆分”之前已經進行了“捆綁拆分”,因此你可能已經知道這部分究竟有多少代碼。
只與結帳有關的代碼是 7 KB,其余部分是 300 KB。看到這個我會說,我不會想去拆分這個代碼,原因如下:
現在讓我們來看看兩個需要代碼拆分的例子。
我之所以從這里開始講起,是因為它適用于大多數網站,而且介紹起來相對簡單。
我在網站上使用了很多花哨的功能,有一個文件導入了所有需要的 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 的頂部導入了這個文件。
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
根據之前的捆綁拆分的 Webpack 配置,polyfill 將自動被拆分為四個不同的文件,因為這里有四個 npm 包。它們總共約 25 KB,但 90%的瀏覽器都不需要它們,所以有必要進行動態加載。
使用 Webpack 4 和 import() 語法(不要與 import 語法混淆)可以很方便地實現 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); }
如果瀏覽器支持所有功能,那么就渲染頁面,否則的話就導入 polyfill,然后渲染頁面。在瀏覽器中運行這些代碼時,Webpack 的運行時將負責加載這四個 npm 包,在下載和解析它們之后,將調用 render()……
順便說一句,要使用 import(),需要 Babel 的 dynamic-import 插件 (https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/)。 另外,正如 Webpack 文檔解釋的那樣,import() 使用了 promise,所以你需要單獨對其進行 polyfill。
這個很簡單,對嗎?下面來點稍微有難度的。
回到 Alice 的例子,我們假設網站有一個“管理”功能,賣家可以登錄并管理他們的商品。
這部分有很多精彩的功能,大量的圖表,需要很多 npm 大圖表庫。因為已經在進行了捆綁拆分,所以它們都是 100 KB 左右的文件。
目前,我的路由設置是當用戶訪問 /admin 時,將會渲染<AdminPage>。當 Webpack 將所有內容捆綁在一起時,它會找到 import AdminPage from ./AdminPage.js,然后說,“我需要將它包含在初始化代碼中”。
但我們不希望它這樣。我們需要使用動態導入,例如 import(‘/AdminPage.js’),這樣 Webpack 就知道要進行動態加載了。
這很酷,不需要做任何配置。
因此,我可以創建另一個組件,當用戶訪問 /admin 時就會渲染這個組件,而不是直接引用 AdminPage。它看起來可能像這樣:
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;
這個概念很簡單。在加載這個組件時(意味著用戶在訪問 /admin),我們將動態加載./AdminPage.js,然后在 state 中保存對該組件的引用。
在等待<AdminPage>加載時,我們只是在 render() 方法中渲染<div> Loading... </div>,或者在加載完成時渲染<AdminPage>,并保存在 state 中。
我自己這樣做是為了好玩,但在現實世界中,你可以使用 react-loadable,正如 React 文檔(https://reactjs.org/docs/code-splitting.html) 中關于代碼拆分的描述那樣。
以上就是所有我想說的話,簡單地說就是:
如果用戶會多次訪問你的網站,請將你的代碼拆分為很多小文件。如果你的網站有些部分是大部分用戶不會訪問到的,請動態加載這些代碼。
英文原文
https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
質文章,第一時間送達!
學習全文大概需要 12分鐘,內容實戰性較強。
1. 前言
本篇將基于Python 3.7+Django 3.0結合Vue.js前端框架,為大家介紹如何基于這三者的技術棧來實現一個前端后離的Web開發項目。為了簡化,方便讀者理解,本文將以開發一個單體頁面應用作為實戰演示。
2. 先搞清楚什么是前后端分離
在正式開始實戰示例之前,我們有必要先弄清楚一個概念:什么是前后端分離?
前后端分離目前已成為互聯網項目開發的業界標準使用方式,在聊前后端分離之前,相信也有很多讀者,對如何區分前端還是后端,還搞不清楚(是不是讓我戳中了你的痛處了)。本著“致良知”,先科譜一下知識。
通常情況下,我們說的前端,大多是指瀏覽器這一端,一般是用Html+CSS+JS來實現的,所以通常會引申為用Html+CSS+JS寫的大部分程序都是前端,包括App,小程序,H5等。
PS: 在NodeJS出現之后,用NodeJS寫的后端部分,也會被人歸類為前端,為了區分之前的前端,就給他們起了一個名字,叫做“大前端”。
久而久之,人們習慣把Html+CSS+JS,運行在瀏覽器端執行的,稱之為前端。
而Java,C,Python,PHP這些運行在服務器端的,統一稱之為后端。
但,這種以語言為分界點去區分前后端,真的合理么?顯然不合理!
前后端的定義,不應該是以語言來定義,而是應該以它的運行環境,如果是在服務器端,就應該被稱之為后端,代表著你看不見,摸不著。而如果運行在用戶端,就應該被稱之為前端,代表你是可以看得到的。
在不分前后端的時候,無論是Java還是JS,全都是一個人來寫。
為什么現在很多互聯網公司在項目開發時,都建議要進行前后端分離,或者說前后端分離能帶來哪些優勢?(好處多多,這里僅提兩個點)
第一個,并行開發、獨立部署、實現前后端解耦,前后端的進度互不影響,在過去,前后端不分離的情況下,項目代碼耦合嚴重相互影響,且前后端人員工作量分布不均。
第二個,術業有專攻(開發人員分離),以前的JavaWeb項目大多數都是Java程序員又當爹又當媽,又搞前端,又搞后端。前后端分離之后,前端工程師只管前端的事情,后端工程師只管后端的事情。
我們先看看一個 Web 系統,在前后端不分離時架構設計是什么樣的。
用戶在瀏覽器上發送請求,服務器端接收到請求,根據 Header 中的 token 進行用戶鑒權,從數據庫取出數據,處理后將結果數據填入 HTML 模板,返回給瀏覽器,瀏覽器將 HTML 展現給用戶。
而采用前后端分離之后,分離的是人員職責,人員職責分離了,因此架構也發生變化。
前后端分離后,前端人員和后端人員約定好接口,前端人員不用再關心業務處理是怎么回事,他只需要把界面做好就可以了,后端人員也不用再關系前端界面是什么樣的,他只需要做好業務邏輯處理即可。
小結一下,前后端分離是什么?
前后端分離是一種架構模式,或者說是最佳實踐,它主張將前端開發人員和后端開發人員的工作進行解耦,盡量減少他她們之間的交流成本,幫助他她們更能專注于自己擅長的工作。
PS: 本篇實戰示例,使用Vue.js作為前端框架,代替Django本身自帶的模板引擎,Django則作為服務端提供API接口,從而實現前后端分離。
3. 環境準備
本實戰示例,基礎環境對應安裝版本如下:
Python 3.7.4
Mysql 5.7
Pycharm (建議專業版)
Node
PS: 其中Python、Mysql、Pycharm、Node安裝過程皆較為簡單,不是本文介紹重點,讀者可直接參考官網安裝方法。
4. 新建獨立的虛擬開發環境
1、創建一個用于Django項目開發的獨立虛擬環境,切換到本地開發目錄,輸入如下命令:
python3 -m venv venv
2、創建完成后,目錄結構如下:
? 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、進入到bin目錄,輸入命令source activate 命令,激活虛擬環境。
4、虛擬環境激活后,如上圖所示。接下來,在虛擬環境安裝Django庫。
安裝Django (最新版本為3.0)
(venv) ? pip install Django
Django 項目源碼:
https://github.com/django/django
Django3.0 版本特性可查閱官網:
https://docs.djangoproject.com/en/3.0/releases/3.0/
5、安裝完成后,可檢查一下版本信息:
(venv) ? python
Python 3.7.4 (default, Jul 9 2019, 18:15:00)
[Clang 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> print(django.get_version)
3.0
可以發現,在虛擬環境中已經成功安裝好了Django 3.0。
5. 創建Django后端項目
1、創建Django項目,采用Pycharm或者命令行創建皆可。此處,以命令行方式作為演示,項目名為django_vue。
(venv) ? django-admin startproject django_vue
2. Django項目創建完成后,目錄結構如下所示。
├── django_vue
│ ├── django_vue
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ └── manage.py
3、執行同步數據庫文件(Django默認數據庫為db.sqlite3),執行同步過程如下:
(venv) ? python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running 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、啟動Django Server ,驗證默認配置是否正常。
(venv) ? python manage.py runserver 0.0.0.0:8000
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
December 15, 2019 - 08:36:28
Django 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、打開瀏覽器,訪問http://localhost:8000,一切正常的話,可見到如下界面。
6. 將Django數據庫更換為Mysql
1、假設在前面,我們已經安裝配置好了Mysql,輸入如下命令進入到Mysql。
mysql -u root -p
2、創建數據庫,數據庫取名為django_vue_db,并設置字符集為utf-8。
mysql> CREATE DATABASE django_vue_db CHARACTER SET utf8;
Query OK, 1 row affected (0.01 sec)
3、安裝myslqclient庫
(venv) ? pip install mysqlclient
4、配置settings.py文件,配置Mysql數據庫引擎。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_vue_db',
'USER': 'root',
'PASSWORD': 'xxxxxxx',
'HOST': '127.0.0.1',
}
}
```
5、執行同步操作,將數據遷移到Mysql。
python manage.py migrate
6、驗證是否切庫成功,進入到Mysql客戶端,查看django初化表是否有生成。
mysql> use django_vue_db;
Database changed
mysql> 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、運行Django Server,重新訪問http://localhost:8000。
python manage.py runserver 0.0.0.0:8000
如果能正常訪問,過程沒有報錯,說明切換數據庫已經成功了。
7. 創建Django實戰項目App
1、創建Django App,進入django_vue項目主目錄,輸入如下命令:
(venv) ? python manage.py startapp api_test
2、App創建完成后,目錄結構如下所示:
├── api_test
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
并把api_test加入到settings文件中的installed_apps列表里:
```python
INSTALLED_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里我們簡單寫一個model如下:
```python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
class 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
```
只有兩個字段,書名book_name和添加時間add_time。如果沒有指定主鍵的話Django會自動新增一個自增id作為主鍵。
4、在api_test目錄下的views里我們新增兩個接口,一個是show_books返回所有的書籍列表(通過JsonResponse返回能被前端識別的json格式數據),二是add_book接受一個get請求,往數據庫里添加一條book數據。
```python
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from django.core import serializers
from django.http import JsonResponse
import 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的幫忙下,我們的接口實際上不需要自己去組織SQL代碼。
5、在api_test目錄下,新增一個urls.py文件,把我們新增的兩個接口添加到路由里:
from django.conf.urls import url, include
from .views import *
urlpatterns = [
url(r"add_book$", add_book, ),
url(r"show_books$", show_books, ),
]
```
6、我們還要把api_test下的urls添加到項目django_vue下的urls中,才能完成路由:
```python
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include
from django.contrib import admin
from django.views.generic import TemplateView
import api_test.urls
urlpatterns = [
url(r"^admin/", admin.site.urls),
url(r'^api/', include(api_test.urls)),
]
```
7、在項目的根目錄,輸入命令:
python manage.py makemigrations api_test
python manage.py migrate
8、查詢數據庫,看到book表已經自動創建了:
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、在項目的根目錄,輸入命令:
python manage.py runserver 0.0.0.0:800
啟動服務,通過httpie測試一下我們剛才寫的兩個接口。
10、通過調用接口向Django App中添加兩條書名記錄。
```shell
? http http://127.0.0.1:8000/api/add_book\?book_name\=mikezhou_talk
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: application/json
Date: Sun, 15 Dec 2019 09:11:12 GMT
Server: WSGIServer/0.2 CPython/3.7.4
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"error_num": 0,
"msg": "success"
}
? http http://127.0.0.1:8000/api/add_book\?book_name\=測試開發技術
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: application/json
Date: Sun, 15 Dec 2019 09:11:44 GMT
Server: WSGIServer/0.2 CPython/3.7.4
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"error_num": 0,
"msg": "success"
}
```
11、通過調用接口,顯示Django App中所有書名列表:
```shell
? http http://127.0.0.1:8000/api/show_books
HTTP/1.1 200 OK
Content-Length: 305
Content-Type: application/json
Date: Sun, 15 Dec 2019 09:13:48 GMT
Server: WSGIServer/0.2 CPython/3.7.4
X-Content-Type-Options: nosniff
X-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": "測試開發技術"
},
"model": "api_test.book",
"pk": 2
}
],
"msg": "success"
}
```
8. 新建Vue.js前端項目
1、有關Vue的模塊(包括vue)可以使用node自帶的npm包管理器安裝。推薦使用淘寶的 cnpm 命令行工具代替默認的 npm。
npm install -g cnpm --registry=https://registry.npm.taobao.org
2、先用cnpm安裝vue-cli腳手架工具(vue-cli是官方腳手架工具,能迅速幫你搭建起vue項目的框架):
cnpm install -g vue-cli
3、安裝好后,在django_vue項目根目錄下,新建一個前端工程目錄:
vue-init webpack frontend
在創建項目的過程中會彈出一些與項目相關的選項需要回答,按照真實情況進行輸入即可。
4、安裝 vue 依賴模塊
cd frontend
cnpm install
cnpm install vue-resource
cnpm install element-ui
5、現在我們可以看到整個文件目錄結構是這樣的:
本文為了讀者方便查看,是直接將vue前端工程放在django項目目錄下,實際多人協作開發過程中,完全是可以放在不同代碼倉庫下面的。
6、在frontend目錄src下包含入口文件main.js,入口組件App.vue等。后綴為vue的文件是Vue.js框架定義的單文件組件,其中標簽中的內容可以理解為是類html的頁面結構內容。
7、在src/component文件夾下新建一個名為Home.vue的組件,通過調用之前在Django上寫好的api,實現添加書籍和展示書籍信息的功能。在樣式組件上我們使用了餓了么團隊推出的element-ui,這是一套專門匹配Vue.js框架的功能樣式組件。由于組件的編碼涉及到了很多js、html、css的知識,并不是本文的重點,因此在此只貼出部分代碼:
Home.vue文件代碼:
```vue
<template>
<div class="home">
<el-row display="margin-top:10px">
<el-input v-model="input" placeholder="請輸入書名" 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="編號" 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="添加時間" 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('新增書籍失敗,請重試')
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路由中:
```js
import 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文件中,導入element-ui、vue-resource庫。
```js
import 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、如果出現跨域問題,需要在Django層注入header,用Django的第三方包django-cors-headers
來解決跨域問題:
pip install django-cors-headers
settings.py 修改:
```python
MIDDLEWARE = [
'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
啟動node自帶的服務器,瀏覽器會自動打開, 我們能看到頁面:
13、嘗試新增書籍,如填入:“自動化測試實戰寶典”,新增的書籍信息會實時反映到頁面的列表中,這得益于Vue.js的數據雙向綁定特性。
14、在前端工程frontend目錄下,輸入npm run build
,如果項目沒有錯誤的話,就能夠看到所有的組件、css、圖片等都被webpack自動打包到dist目錄下了:
9. 整合Django和Vue.js前端
目前我們已經分別完成了Django后端和Vue.js前端工程的創建和編寫,但實際上它們是運行在各自的服務器上,和我們的要求是不一致的。因此我們須要把Django的TemplateView指向我們剛才生成的前端dist文件即可。
1、 找到project目錄的urls.py,使用通用視圖創建最簡單的模板控制器,訪問 『/』時直接返回 index.html:
```python
urlpatterns = [
url(r"^admin/", admin.site.urls),
url(r'^api/', include(api_test.urls)),
url(r'^$', TemplateView.as_view(template_name="index.html")),
]
```
2、上一步使用了Django的模板系統,所以需要配置一下模板使Django知道從哪里找到index.html。在project目錄的settings.py下:
```python
TEMPLATES = [
{
'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、 我們還需要配置一下靜態文件的搜索路徑。同樣是project目錄的settings.py下:
```python
# Add for vuejs
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "frontend/dist/static"),
]
```
4、 配置完成,我們在project目錄下輸入命令python manage.py runserver
,就能夠看到我們的前端頁面在瀏覽器上展現:
注意此時服務的端口已經是Django服務的8000而不是node服務的8080了,說明我們已經成功通過Django集成了Vue前端工程。
該實戰示例為大家充分展示了現在主流的前后端分離方式,由前端框架,如Vue.js來構建實現前端界面,再通過后端框架,如Django來實現API數據提供,兩者通過接口進行通訊、相輔相成、最終實現一個完整Web項目。
聲明:前后端分離圖片來源于網絡,如有侵權,請聯系刪除。
個網站該如何以最佳的方式向用戶發送資源文件?有很多不同的場景,不同的技術和不同的術語。在這篇文章里,我希望能夠讓你明白:哪種文件分割策略最適合你的網站和用戶,以及如何實現。
根據 Webpack 術語表,有兩種不同的文件分割類型。它們看起來似乎可以互換,但顯然不行:
第二種方法看起來更有吸引力,不是嗎?事實上,有很多文章似乎都假設這是拆分 JavaScript 文件唯一有價值的方案。但我想要告訴你的是,對于很多網站來說,第一種方法更有價值,而且它應該是你首先要考慮的。
捆綁拆分背后的想法非常簡單。如果你有一個巨大的文件,哪怕只是修改了一行代碼,用戶也必須再次下載整個文件。但是,如果你將它分成兩個文件,那么用戶只需要下載被修改的那個文件,瀏覽器會從緩存中獲取另一個文件。
捆綁拆分與緩存有關,因此對于首次訪問網站的用戶來說,有沒有拆分其實并沒有什么不同。
對于頻繁訪問網站的用戶來說,要衡量捆綁拆分所帶來的性能提升可能也很棘手,但我們必須這樣做!
我需要一個表格來記錄性能數據。下面是上述提到的場景:
假設我們的 JavaScript 包大小是 400 KB,只包含 main.js 單個文件。
我們的 Webpack 配置如下(我省略了不相關的配置):
const path = require('path');
?
module.exports = {
entry: path.resolve(__dirname, 'src/index.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
};
每個禮拜,當我們做出一些變更時,這個包的 contenthash 就會發生變化。因此,每周 Alice 訪問我們的網站時必須下載新的 400 KB 文件。
我們把這些數字記錄在表格中,它看起來就像這樣。
下載量總共是 4.12 MB,為期 10 周。
但我們可以做得更好。
拆分 vendor 包
現在,我們將包拆分為 main.js 和 vendor.js 文件。
這很簡單:
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 努力為你做最好的事情,甚至都不需要告訴它你想要如何拆分捆綁包。
有人說,“這樣看起來很整潔,不錯,Webpack!”
也有人說,“你都對我的包做了什么?”
設置 optimization.splitChunks.chunks ='all'意味著“將 node_modules 所有內容都放入名為 vendors~main.js 的文件中”。
經過這個基本的捆綁拆分,Alice 每次訪問網站時仍然需要下載 200 KB 的 main.js 新文件,然后分別在第 1 周,第 8 周和第 5 周下載 200 KB 的 vendor.js 文件。
現在的下載量總共是 2.64 MB。
減少了 36%。在配置中加了五行代碼,效果還不錯。
這樣的性能提升似乎有點微不足道,因為它是 10 周加起來的總和,但不管怎樣,向用戶發送的字節數確確實實減少了 36%,我們應該為自己感到自豪。
但我們可以做得更好。
拆分每個 npm 包
vendors.js 遇到了與原來 main.js 文件相同的問題——對文件的一部分做出變更就必須重新下載整個文件。
那么為什么不為每個 npm 包提供單獨的文件呢?這很容易做到。
所以讓我們將 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/)對此做出了很好的解釋,我會大致解釋一下 groovy 的部分,因為我在這個上面花了很多時間:
Alice 每周仍然會重新下載 200 KB 的 main.js 文件,并且在她第一次訪問網站時仍然會下載 200 KB 的 npm 軟件包,但她絕不會下載相同的軟件包兩次。
現在的下載總量是 2.24 MB,與基線相比減少了 44%。
我在想是否有可能減少 50%?
拆分應用程序代碼
現在讓我們回到可憐的 Alice 一次又一次下載的 main.js 文件。
我之前提到過,我們的網站上有兩個截然不同的部分:產品列表頁面和產品詳細信息頁面。每個部分不一樣的代碼為 25 KB(共享代碼為 150 KB)。
“產品詳細信息”頁面現在并沒有發生太大變化,因此,如果我們將其變為單獨的文件,大多數時候可以從緩存中獲取它。
另外,我們有一個巨大的內聯 SVG 文件用于渲染圖標,大小為 25 KB,而且很少會發生改動。
我們手動添加了一些條目,告訴 Webpack 為每一項創建一個文件。
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 還會為 ProductList 和 ProductPage 之間共享的內容創建文件,這樣我們就不會得到重復的代碼。
這樣就可以為親愛的 Alice 節省 50 KB 的下載量。
現在的總下載量只有 1.815 MB!
我們已經為 Alice 節省了高達 56%的下載量,在我們的理論場景中,這種情況可以一直持續下去。
截止到目前,我們只是通過修改 Webpack 配置來實現這一切——我們沒有對應用程序代碼進行任何更改。
我們的目標是將應用程序拆分為合理的小文件,讓用戶下載更少的代碼。
因此,接下來我們要進入代碼拆分,但首先我想要解決你現在想到的三個問題。問題 1:大量的網絡請求不是更慢嗎?
對于這個問題,答案是一個非常響亮的“不”。
在 HTTP/1.1 時代或許是這種情況,但對于 HTTP/2 來說并非如此。
盡管一些著名的文章得出“即使使用 HTTP/2,下載太多文件仍然較慢”的結論,但在這些文章中,他們所謂的“太多”文件是指“數百個”文件。所以請記住,如果你有數百個文件,可能會達到并發上限。
問題 2:每個 Webpack 捆綁包中不是有樣板代碼?
是的。
問題 3:如果有多個小文件,不就失去了壓縮的優勢了嗎?
是的。
好吧,這就是說:
接下來讓我們做一下量化,這樣就可以確切地知道性能被磨損了多少。
我做了一個測試,將 190KB 的文件拆分成 19 個小文件,這樣發送給瀏覽器的總字節數大約增加了 2%。
在第一次訪問時增加 2%,但在以后訪問可以減少 60%,所以可以說完全沒有磨損。
我針對 1 個文件和 19 個文件分別進行了測試,并基于不同的網絡,包括 HTTP/1.1。
這是結果表格,我想這足以說明“更多文件會更好”:
在 3G 和 4G 網絡上,當有 19 個文件時,總的加載時間縮短了 30%。
當然,這些數據帶有一定的噪音。例如,第二次在 4G 網絡上的加載時間為 646 毫秒,過了兩次之后需要 1116 毫秒——多了 73%。因此,聲稱 HTTP/2“快 30%”似乎有點心虛的感覺。
我制作這張表來是想要量化 HTTP/2 的差異,但看來我唯一能說的是“它可能沒有顯著差異”。
真正的驚喜是最后兩行,我原本認為舊的 Windows 和 HTTP/1.1 會很慢。
這就是我要說的有關捆綁拆分的一切。我認為這種方法的唯一缺點是要不斷地說服人們,加載大量小文件是沒有問題的。
現在,讓我們談談另一種類型的文件拆分。
代碼拆分(不加載不需要的代碼)
這種方法可能只對某些網站有用。
我發明了 20/20 規則:如果你的網站的某些部分只有 20%的用戶訪問,而這部分超過了整個網站 20%的 JavaScript,那么你應該按需加載這些代碼。
顯然,因為存在更復雜的場景,所以這個數字顯然需要做出調整。但關鍵在于,肯定存在一個平衡點,到了這個平衡點,代碼拆分對于你的網站來說可能就沒有意義了。
假設你有一個購物網站,你想知道是否應該對“結帳”代碼進行拆分,因為只有 30%的用戶會進行這個操作。
你需要弄清楚有多少代碼是只與結賬這個功能有關的。因為在進行“代碼拆分”之前已經進行了“捆綁拆分”,因此你可能已經知道這部分究竟有多少代碼。
只與結帳有關的代碼是 7 KB,其余部分是 300 KB。看到這個我會說,我不會想去拆分這個代碼,原因如下:
現在讓我們來看看兩個需要代碼拆分的例子。
我之所以從這里開始講起,是因為它適用于大多數網站,而且介紹起來相對簡單。
我在網站上使用了很多花哨的功能,有一個文件導入了所有需要的 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 的頂部導入了這個文件。
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
根據之前的捆綁拆分的 Webpack 配置,polyfill 將自動被拆分為四個不同的文件,因為這里有四個 npm 包。它們總共約 25 KB,但 90%的瀏覽器都不需要它們,所以有必要進行動態加載。
使用 Webpack 4 和 import() 語法(不要與 import 語法混淆)可以很方便地實現 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);
}
如果瀏覽器支持所有功能,那么就渲染頁面,否則的話就導入 polyfill,然后渲染頁面。在瀏覽器中運行這些代碼時,Webpack 的運行時將負責加載這四個 npm 包,在下載和解析它們之后,將調用 render()……
順便說一句,要使用 import(),需要 Babel 的 dynamic-import 插件 (https://babeljs.io/docs/en/babel-plugin-syntax-dynamic-import/)。 另外,正如 Webpack 文檔解釋的那樣,import() 使用了 promise,所以你需要單獨對其進行 polyfill。
這個很簡單,對嗎?下面來點稍微有難度的。
基于路由的動態加載(特定于 React)
回到 Alice 的例子,我們假設網站有一個“管理”功能,賣家可以登錄并管理他們的商品。
這部分有很多精彩的功能,大量的圖表,需要很多 npm 大圖表庫。因為已經在進行了捆綁拆分,所以它們都是 100 KB 左右的文件。
目前,我的路由設置是當用戶訪問 /admin 時,將會渲染<AdminPage>。當 Webpack 將所有內容捆綁在一起時,它會找到 import AdminPage from ./AdminPage.js,然后說,“我需要將它包含在初始化代碼中”。
但我們不希望它這樣。我們需要使用動態導入,例如 import(‘/AdminPage.js’),這樣 Webpack 就知道要進行動態加載了。
這很酷,不需要做任何配置。
因此,我可以創建另一個組件,當用戶訪問 /admin 時就會渲染這個組件,而不是直接引用 AdminPage。它看起來可能像這樣:
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;
這個概念很簡單。在加載這個組件時(意味著用戶在訪問 /admin),我們將動態加載./AdminPage.js,然后在 state 中保存對該組件的引用。
在等待<AdminPage>加載時,我們只是在 render() 方法中渲染<div> Loading... </div>,或者在加載完成時渲染<AdminPage>,并保存在 state 中。
我自己這樣做是為了好玩,但在現實世界中,你可以使用 react-loadable,正如 React 文檔(https://reactjs.org/docs/code-splitting.html) 中關于代碼拆分的描述那樣。
以上就是所有我想說的話,簡單地說就是:
如果用戶會多次訪問你的網站,請將你的代碼拆分為很多小文件。如果你的網站有些部分是大部分用戶不會訪問到的,請動態加載這些代碼。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。