管理系統(tǒng)源碼拆解)
SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 這套技術(shù)棧過(guò)去兩年幾乎成了Java Web教學(xué)管理系統(tǒng)的“標(biāo)準(zhǔn)答案”。項(xiàng)目源碼我前后看過(guò)不下二十套大多數(shù)的問(wèn)題不是功能不夠而是代碼組織太亂根本沒(méi)法教學(xué)。這套系統(tǒng)源碼算是我見(jiàn)到的少數(shù)把“教學(xué)屬性”和“工程規(guī)范”平衡得不錯(cuò)的項(xiàng)目今天就從技術(shù)選型、代碼結(jié)構(gòu)、前后端聯(lián)調(diào)和部署落地四個(gè)維度拆一拆講清楚這套系統(tǒng)值不值得借鑒以及你自己要搭一套類似的應(yīng)該注意什么。1. 為什么這套技術(shù)棧會(huì)成為教學(xué)管理系統(tǒng)的主流選擇先說(shuō)結(jié)論SpringBoot2 Vue3 MyBatis-Plus MySQL8.0不是性能最優(yōu)解也不是技術(shù)最前沿的組合但它是教學(xué)場(chǎng)景下投入產(chǎn)出比最高的一套方案。我見(jiàn)過(guò)很多學(xué)校和企業(yè)培訓(xùn)項(xiàng)目用SSHStruts2 Spring Hibernate或者JSP Servlet的老框架代碼能跑但是學(xué)生看完之后完全不知道外面企業(yè)實(shí)際在用什么。1.1 前后端分離是教學(xué)系統(tǒng)最該有的姿態(tài)傳統(tǒng)的JSP項(xiàng)目前端頁(yè)面嵌在后端工程里改一個(gè)按鈕樣式都要重啟Tomcat。這套系統(tǒng)采用Vue3 SpringBoot的前后端完全分離架構(gòu)前端是獨(dú)立的工程通過(guò)HTTP接口和后端通信這一點(diǎn)的教學(xué)價(jià)值非常大——它讓學(xué)生從一開(kāi)始就建立起“接口即契約”的認(rèn)知。前端工程用Vite構(gòu)建開(kāi)發(fā)環(huán)境下通過(guò)代理轉(zhuǎn)發(fā)解決跨域問(wèn)題生產(chǎn)環(huán)境通過(guò)Nginx反向代理統(tǒng)一入口。這種模式是目前中小型團(tuán)隊(duì)最標(biāo)準(zhǔn)的開(kāi)發(fā)協(xié)作方式前端同學(xué)只管頁(yè)面和交互后端同學(xué)只管接口和數(shù)據(jù)處理兩邊通過(guò)Swagger或者YApi維護(hù)接口文檔互不阻塞。1.2 MyBatis-Plus把數(shù)據(jù)訪問(wèn)層的教學(xué)成本降到了最低如果用的是原生MyBatis學(xué)生光寫(xiě)Mapper XML就要崩潰尤其是多表關(guān)聯(lián)查詢和動(dòng)態(tài)SQL能直接勸退一批初學(xué)者。MyBatis-Plus的核心價(jià)值在于單表CRUD完全不用寫(xiě)SQL內(nèi)置的BaseMapper提供了基礎(chǔ)的增刪改查方法分頁(yè)查詢用Page對(duì)象一封裝就行。這套系統(tǒng)的用戶表、角色表、課程表這類基礎(chǔ)實(shí)體的數(shù)據(jù)訪問(wèn)層代碼量比原生MyBatis少了一半以上。學(xué)生在理解了ORM基本思想后可以直接把精力放在業(yè)務(wù)邏輯上而不是糾結(jié)SQL怎么拼接。1.3 MySQL8.0帶來(lái)的新特性值得在教學(xué)中重點(diǎn)展示MySQL5.7在不少老項(xiàng)目中還在用但8.0的窗口函數(shù)、公用表表達(dá)式CTE、默認(rèn)字符集utf8mb4這些特性在報(bào)表統(tǒng)計(jì)類的教學(xué)模塊中非常實(shí)用。這套系統(tǒng)選擇MySQL8.0不是趕時(shí)髦而是課程管理、成績(jī)分析這類模塊確實(shí)用得上。舉個(gè)例子成績(jī)排名功能如果用5.7的寫(xiě)法得用用戶變量模擬行號(hào)SQL寫(xiě)出來(lái)又長(zhǎng)又繞。8.0里直接一個(gè)ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC)就搞定了語(yǔ)義清晰學(xué)生一看就懂。2. 后端代碼結(jié)構(gòu)拆解SpringBoot2工程是怎么組織起來(lái)的拿到這套源碼的第一件事建議先看包結(jié)構(gòu)。好的包結(jié)構(gòu)本身就是一份教學(xué)文檔它告訴學(xué)習(xí)者一個(gè)企業(yè)級(jí)項(xiàng)目應(yīng)該怎么劃分職責(zé)邊界。2.1 分層次的包結(jié)構(gòu)是后端代碼的第一課這套系統(tǒng)的后端包結(jié)構(gòu)遵循了標(biāo)準(zhǔn)的三層架構(gòu) 領(lǐng)域劃分com.example.teachingsystem ├── controller # 控制層接收請(qǐng)求、參數(shù)校驗(yàn)、返回結(jié)果 ├── service # 業(yè)務(wù)層核心業(yè)務(wù)邏輯 │ ├── impl # 業(yè)務(wù)實(shí)現(xiàn)類 │ └── ... ├── mapper # 數(shù)據(jù)訪問(wèn)層繼承BaseMapper的接口 ├── entity # 實(shí)體類對(duì)應(yīng)數(shù)據(jù)庫(kù)表 ├── dto # 數(shù)據(jù)傳輸對(duì)象接口入?yún)⒑统鰠?├── vo # 視圖對(duì)象前端頁(yè)面展示數(shù)據(jù) ├── config # 配置類攔截器、跨域、Swagger等 ├── common # 通用類統(tǒng)一返回結(jié)果、異常處理、工具類 └── TeachingApplication.java # SpringBoot啟動(dòng)類這種結(jié)構(gòu)看起來(lái)簡(jiǎn)單但很多自學(xué)框架的人就是栽在這上面。有人把業(yè)務(wù)邏輯全寫(xiě)在Controller里一個(gè)接口方法幾百行有人實(shí)體類和DTO混用數(shù)據(jù)庫(kù)字段直接暴露給前端。這套系統(tǒng)在這一點(diǎn)上做了良好的示范Controller只做參數(shù)接收和結(jié)果包裝業(yè)務(wù)邏輯下沉到Service層數(shù)據(jù)訪問(wèn)統(tǒng)一走M(jìn)apper。2.2 統(tǒng)一返回結(jié)果和全局異常處理這兩個(gè)類值得單獨(dú)拎出來(lái)講前后端分離的項(xiàng)目最忌諱的就是每個(gè)接口返回的數(shù)據(jù)格式都不一樣。前端拿到的數(shù)據(jù)有時(shí)候是{code: 0, data: {...}}有時(shí)候是{success: true, result: [...]}聯(lián)調(diào)的時(shí)候光對(duì)齊字段就能耗掉半天。這套系統(tǒng)定義了一個(gè)統(tǒng)一的Result類結(jié)構(gòu)固定為public class ResultT { private Integer code; // 狀態(tài)碼200成功500失敗 private String message; // 提示信息 private T data; // 返回?cái)?shù)據(jù) }所有Controller的返回值都包裝成這個(gè)對(duì)象前端在封裝Axios請(qǐng)求的時(shí)候只需要在攔截器里統(tǒng)一處理一次就能拿到真正需要的數(shù)據(jù)。這種約定雖然簡(jiǎn)單但是能避免大量聯(lián)調(diào)時(shí)的低級(jí)溝通成本。全局異常處理用的是RestControllerAdvice注解業(yè)務(wù)異常、參數(shù)校驗(yàn)異常、系統(tǒng)異常分別處理返回對(duì)應(yīng)的錯(cuò)誤碼和提示信息。沒(méi)有這個(gè)類的話后端一旦報(bào)錯(cuò)前端拿到的就是一堆看不懂的堆棧信息用戶直接看到500頁(yè)面體驗(yàn)非常糟糕。2.3 權(quán)限設(shè)計(jì)基于Spring Security JWT的登錄態(tài)管理教學(xué)管理系統(tǒng)的權(quán)限模型一般分三種角色管理員、教師、學(xué)生。這套系統(tǒng)的權(quán)限控制采用了Spring Security JWT的方案沒(méi)有用Session這一點(diǎn)很符合當(dāng)下前后端分離項(xiàng)目的實(shí)際情況。用戶登錄成功后后端生成一個(gè)JWT令牌返回給前端前端存儲(chǔ)在本地之后每次請(qǐng)求都在請(qǐng)求頭里帶上Authorization: Bearer token。后端通過(guò)攔截器解析令牌識(shí)別用戶身份和角色再根據(jù)角色判斷是否有權(quán)限訪問(wèn)對(duì)應(yīng)的接口。JWT方案的教學(xué)意義在于學(xué)生能理解無(wú)狀態(tài)認(rèn)證的基本原理也能理解Token過(guò)期、刷新、注銷這些實(shí)際生產(chǎn)環(huán)境必須考慮的問(wèn)題。我記得這套源碼里Token的過(guò)期時(shí)間設(shè)置的是兩小時(shí)還預(yù)留了刷新接口考慮得比較周全。3. 前端Vue3工程的實(shí)現(xiàn)思路與常見(jiàn)坑位3.1 組合式APIComposition API讓組件邏輯更集中這套系統(tǒng)的前端用的是Vue3的組合式API沒(méi)有沿襲Vue2的選項(xiàng)式API寫(xiě)法。以課程管理頁(yè)面為例課程列表的加載、搜索條件的響應(yīng)式數(shù)據(jù)、分頁(yè)切換事件全都在setup函數(shù)中通過(guò)ref、reactive、onMounted組織在一起。相比Vue2的data、methods、computed分塊書(shū)寫(xiě)這種寫(xiě)法最大的好處是與某個(gè)功能相關(guān)的代碼聚在一起修改或排查問(wèn)題時(shí)不用在多個(gè)選項(xiàng)間來(lái)回跳轉(zhuǎn)。我見(jiàn)過(guò)一些Vue3項(xiàng)目表面上是Vue3寫(xiě)法還是Vue2那套選項(xiàng)式API——能用但完全沒(méi)有發(fā)揮出Vue3的優(yōu)勢(shì)。這套系統(tǒng)的做法值得學(xué)習(xí)新代碼統(tǒng)一用組合式API自定義業(yè)務(wù)邏輯抽到hooks目錄下比如useUserList、useCourseManage組件里只保留和模板渲染直接相關(guān)的邏輯代碼可讀性大大提升。3.2 路由守衛(wèi)和動(dòng)態(tài)權(quán)限菜單的實(shí)現(xiàn)邏輯后臺(tái)管理系統(tǒng)幾乎都有一個(gè)需求不同角色登錄后看到的菜單不一樣。學(xué)生登錄看不到用戶管理教師登錄看不到選課審核。這套系統(tǒng)的實(shí)現(xiàn)路徑是用戶登錄成功后后端返回當(dāng)前用戶的角色和權(quán)限標(biāo)識(shí)前端根據(jù)權(quán)限標(biāo)識(shí)動(dòng)態(tài)生成路由表用router.addRoute動(dòng)態(tài)注冊(cè)路由守衛(wèi)beforeEach中判斷登錄狀態(tài)和頁(yè)面權(quán)限未登錄跳轉(zhuǎn)登錄頁(yè)無(wú)權(quán)限跳轉(zhuǎn)403頁(yè)面這個(gè)邏輯說(shuō)起來(lái)簡(jiǎn)單但實(shí)現(xiàn)上有幾個(gè)容易出問(wèn)題的地方。一是刷新頁(yè)面后路由表丟失需要在App.vue的onMounted或者路由守衛(wèi)里重新拉取用戶信息并重新生成路由二是動(dòng)態(tài)路由和靜態(tài)路由的合并順序必須先添加靜態(tài)路由再添加動(dòng)態(tài)路由否則首次訪問(wèn)動(dòng)態(tài)頁(yè)面會(huì)匹配到404。3.3 Element Plus的二次封裝減少重復(fù)代碼這套系統(tǒng)用的是Element Plus組件庫(kù)但并沒(méi)有直接在頁(yè)面里堆組件而是做了二次封裝。比如分頁(yè)組件原生的Pagination組件每次使用都要寫(xiě)一堆:current-page、:total、current-change綁定這套系統(tǒng)把它封裝成了Pagination組件內(nèi)部封裝好分頁(yè)參數(shù)頁(yè)面里只需要傳一個(gè)數(shù)據(jù)和回調(diào)函數(shù)template div el-table :datatableData.../el-table Pagination :totaltotal :pagequeryParams.pageNum :limitqueryParams.pageSize paginationhandleQuery / /div /template這種封裝的思路比組件本身更有教學(xué)價(jià)值。學(xué)生學(xué)完之后會(huì)意識(shí)到組件封裝不是為了炫技而是為了減少重復(fù)勞動(dòng)讓頁(yè)面代碼更簡(jiǎn)潔。4. MySQL8.0中的核心表設(shè)計(jì)與數(shù)據(jù)訪問(wèn)優(yōu)化4.1 三張核心表的結(jié)構(gòu)設(shè)計(jì)與字段規(guī)范教學(xué)管理系統(tǒng)最核心的業(yè)務(wù)表是這幾張用戶表user、課程表course、選課表student_course。這套系統(tǒng)的表結(jié)構(gòu)設(shè)計(jì)比較規(guī)范幾個(gè)關(guān)鍵點(diǎn)值得學(xué)習(xí)用戶表的主鍵用的是bigint類型的自增id用戶名字段設(shè)置了唯一索引。密碼字段存的是BCrypt加密后的密文絕對(duì)不是明文這一點(diǎn)很多學(xué)生自己做項(xiàng)目時(shí)會(huì)忽略——他們圖省事直接把密碼明文存了這是非常嚴(yán)重的隱患。課程表和選課表之間通過(guò)外鍵邏輯關(guān)聯(lián)注意是邏輯關(guān)聯(lián)沒(méi)有物理外鍵選課表用user_id和course_id的組合字段建立了唯一索引防止學(xué)生重復(fù)選同一門(mén)課。這個(gè)設(shè)計(jì)很巧妙不用在代碼里先查再插而是直接把唯一索引當(dāng)作最后的防線數(shù)據(jù)庫(kù)層面就攔截了重復(fù)數(shù)據(jù)。所有表都包含create_time和update_time兩個(gè)時(shí)間字段MyBatis-Plus的字段填充功能可以自動(dòng)賦值不需要代碼里手動(dòng)set。這個(gè)細(xì)節(jié)在實(shí)際開(kāi)發(fā)中非常常用幾乎每個(gè)業(yè)務(wù)表都要有記錄創(chuàng)建和修改時(shí)間的需求非常適合作為教學(xué)的入門(mén)案例。4.2 MyBatis-Plus的實(shí)用特性分頁(yè)插件和條件構(gòu)造器MyBatis-Plus的分頁(yè)插件是使用頻率最高的功能之一。這套系統(tǒng)的分頁(yè)查詢實(shí)現(xiàn)方式是public PageCourseVO getCoursePage(CourseQuery query) { PageCourseVO page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getCourseName()), Course::getCourseName, query.getCourseName()) .eq(query.getTeacherId() ! null, Course::getTeacherId, query.getTeacherId()) .orderByDesc(Course::getCreateTime); return courseMapper.selectPage(page, wrapper); }條件構(gòu)造器最方便的地方在于like和eq方法的第一個(gè)參數(shù)是布爾表達(dá)式條件不成立時(shí)這個(gè)查詢條件自動(dòng)忽略。這樣就不用在代碼里寫(xiě)一大堆if語(yǔ)句判斷是否拼接條件代碼干凈很多。需要注意分頁(yè)插件的配置在MyBatis-Plus高版本中需要新建一個(gè)配置類注入MybatisPlusInterceptor并添加PaginationInnerInterceptor。少了這步配置selectPage會(huì)返回全表數(shù)據(jù)頁(yè)面上看起來(lái)就像“分頁(yè)失效”了這是新手最常踩的坑。5. 環(huán)境搭建、常見(jiàn)報(bào)錯(cuò)和部署避坑實(shí)錄5.1 從零搭建開(kāi)發(fā)環(huán)境的完整流程我按照這套源碼的文檔在一臺(tái)全新的電腦上從零搭建環(huán)境整個(gè)過(guò)程大概需要四步安裝JDK 1.8或11SpringBoot2對(duì)JDK版本要求不高8以上都能跑。需要注意環(huán)境變量JAVA_HOME的配置如果電腦上有多個(gè)JDK版本Maven打包時(shí)會(huì)崩報(bào)什么錯(cuò)都有非常折磨人。安裝Maven 3.6源碼中帶的pom.xml依賴較多第一次編譯需要下載大量jar包建議在Maven的settings.xml里配置阿里云鏡像源否則等的時(shí)間足夠泡一碗面。安裝MySQL8.0這塊的坑最多。MySQL8.0默認(rèn)的認(rèn)證插件是caching_sha2_password而某些老版本的數(shù)據(jù)庫(kù)連接驅(qū)動(dòng)不支持這個(gè)插件會(huì)報(bào)Unable to load authentication plugin caching_sha2_password。解決方法是換用mysql-connector-java 8.0.x版本的驅(qū)動(dòng)或者創(chuàng)建用戶時(shí)指定mysql_native_password認(rèn)證方式。項(xiàng)目里的application.yml還需要注意修改數(shù)據(jù)庫(kù)名、用戶名、密碼這三個(gè)配置連在一起的一處寫(xiě)錯(cuò)啟動(dòng)就會(huì)報(bào)連接失敗。安裝Node.js 16前端工程需要Node環(huán)境Vite對(duì)Node版本有要求太老版本的Node跑不起來(lái)。安裝完成后在根目錄下載依賴npm install。5.2 數(shù)據(jù)庫(kù)初始化最容易漏掉的細(xì)節(jié)這套系統(tǒng)在sql目錄下提供了一份完整的建表腳本但如果你直接把腳本全部執(zhí)行很可能會(huì)遇到兩個(gè)問(wèn)題一個(gè)是時(shí)區(qū)問(wèn)題。數(shù)據(jù)庫(kù)連接串里SpringBoot2默認(rèn)會(huì)配合參數(shù)serverTimezoneAsia/Shanghai否則插入時(shí)間和當(dāng)前時(shí)間差8個(gè)小時(shí)。如果你在執(zhí)行建表腳本時(shí)發(fā)現(xiàn)create_time存的時(shí)間不對(duì)先檢查連接串的時(shí)區(qū)參數(shù)再檢查MySQL的全局時(shí)區(qū)配置。另一個(gè)是字符集問(wèn)題。建庫(kù)的時(shí)候最好顯式指定utf8mb4字符集CREATE DATABASE teaching CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。如果不指定在Linux環(huán)境下默認(rèn)字符集可能是latin1存中文直接亂碼或者報(bào)Incorrect string value錯(cuò)誤。5.3 前后端聯(lián)調(diào)時(shí)Nginx和跨域問(wèn)題的兩種解法開(kāi)發(fā)模式下前端通過(guò)Vite的代理解決跨域配置在vite.config.js里server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生產(chǎn)模式下把前端打包后的dist目錄交給Nginx托管然后配置反向代理server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 解決Vue Router history模式刷新404問(wèn)題 } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files這一行特別關(guān)鍵。Vue Router用history模式時(shí)直接訪問(wèn)/course/list這樣的路徑Nginx找不到對(duì)應(yīng)的物理文件會(huì)返回404。加上try_files $uri $uri/ /index.html之后所有路徑都會(huì)回退到入口頁(yè)面路由再由前端接管問(wèn)題解決。5.4 項(xiàng)目文檔里沒(méi)寫(xiě)但必須注意的日常維護(hù)點(diǎn)這套系統(tǒng)帶了完整的設(shè)計(jì)文檔這一點(diǎn)在課程設(shè)計(jì)或者畢業(yè)設(shè)計(jì)場(chǎng)景下非常加分——我們當(dāng)年答辯老師第一句話就是“你的項(xiàng)目文檔呢”。但有幾個(gè)文檔里沒(méi)細(xì)說(shuō)、實(shí)際運(yùn)行時(shí)要注意的維護(hù)點(diǎn)JWT密鑰泄漏問(wèn)題源碼里的JWT簽名密鑰是硬編碼在配置里的如果部署到公網(wǎng)環(huán)境一定要改成環(huán)境變量注入否則任何人都可以用你的密鑰偽造登錄令牌。Maven打包后靜態(tài)資源路徑問(wèn)題后端打包成jar包后如果前端文件也放進(jìn)src/main/resources/static目錄訪問(wèn)路徑必須帶/static前綴否則出現(xiàn)404。Tomcat端口沖突開(kāi)發(fā)機(jī)上如果裝了多個(gè)服務(wù)8080端口經(jīng)常被占啟動(dòng)報(bào)Port 8080 was already in use。找到占用進(jìn)程殺掉或者在application.yml里換一個(gè)端口。6. 從源碼到可用系統(tǒng)的二次開(kāi)發(fā)思路這套系統(tǒng)把基礎(chǔ)功能做得很完整用戶登錄注冊(cè)、角色權(quán)限管理、課程管理、選課管理、成績(jī)管理、公告管理每一塊都有對(duì)應(yīng)的前后端代碼。但如果你想把它真正用在自己的場(chǎng)景里有幾塊內(nèi)容值得二次開(kāi)發(fā)。第一個(gè)是數(shù)據(jù)統(tǒng)計(jì)模塊。源碼中的首頁(yè)數(shù)據(jù)看板比較基礎(chǔ)展示的是用戶總數(shù)、課程總數(shù)這些靜態(tài)數(shù)字。如果你想讓首頁(yè)更有說(shuō)服力可以基于MySQL8.0的窗口函數(shù)做一個(gè)“近7天選課趨勢(shì)”“各學(xué)院選課排行”這類動(dòng)態(tài)圖表后端提供接口前端用ECharts渲染這個(gè)組合在畢設(shè)答辯中是非常亮眼的亮點(diǎn)。第二個(gè)是文件上傳功能。教學(xué)系統(tǒng)經(jīng)常需要教師上傳課件、學(xué)生提交作業(yè)源碼里在這塊只實(shí)現(xiàn)了最簡(jiǎn)單的單文件上傳存儲(chǔ)在本地磁盤(pán)。如果用戶量上來(lái)需要改造為按日期分目錄存儲(chǔ)、限制文件大小、校驗(yàn)文件類型、支持圖片預(yù)覽和Office在線預(yù)覽甚至接入對(duì)象存儲(chǔ)服務(wù)。第三個(gè)是消息通知。真正好用的教學(xué)系統(tǒng)應(yīng)該有選課成功通知、成績(jī)發(fā)布通知、課程變動(dòng)通知??梢砸隬ebSocket實(shí)現(xiàn)服務(wù)端消息推送也可以簡(jiǎn)化一點(diǎn)用數(shù)據(jù)庫(kù)輪詢的方式做消息中心每次進(jìn)入系統(tǒng)拉取未讀消息。這些二次開(kāi)發(fā)的方向比直接改原有功能更有教學(xué)價(jià)值。學(xué)生能從“會(huì)跑demo”進(jìn)階到“會(huì)設(shè)計(jì)功能模塊”這才是源碼學(xué)習(xí)最重要的收獲。這套SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的教學(xué)管理系統(tǒng)代碼組織和業(yè)務(wù)覆蓋都做得不錯(cuò)配合完整文檔很適合作為課程設(shè)計(jì)參考或者作為學(xué)習(xí)前后端分離開(kāi)發(fā)第一個(gè)完整項(xiàng)目來(lái)精讀。上手時(shí)建議先看文檔里的系統(tǒng)設(shè)計(jì)部分了解功能邊界和技術(shù)方案再對(duì)照表結(jié)構(gòu)和實(shí)體類理解數(shù)據(jù)模型接著是后端Service層的業(yè)務(wù)邏輯最后是前端頁(yè)面如何調(diào)用接口、組織數(shù)據(jù)。按這個(gè)順序讀源碼效率會(huì)高很多。