作為后端開放人員,最煩的事就是自己寫接口文檔和別人沒有寫接口文檔,不管是前端還是后端開發,多多少少都會被接口文檔所折磨,前端會抱怨后端沒有及時更新接口文檔,而后端又會覺得編寫接口文檔太過麻煩。Swagger 可以較好的接口接口文檔的交互問題,以一套標準的規范定義接口以及相關的信息,就能做到生成各種格式的接口文檔,生成多種語言和客戶端和服務端的代碼,以及在線接口調試頁面等等。只需要更新 Swagger 描述文件,就能自動生成接口文檔,做到前端、后端聯調接口文檔的及時性和便利性。
Swagger 是一個規范且完整的框架,用于生成、描述、調用和可視化 RESTful 風格的 Web 服務。
Swagger 的目標是對 REST API 定義一個標準且和語言無關的接口,可以讓人和計算機擁有無須訪問源碼、文檔或網絡流量監測就可以發現和理解服務的能力。當通過 Swagger 進行正確定義,用戶可以理解遠程服務并使用最少實現邏輯與遠程服務進行交互。與為底層編程所實現的接口類似,Swagger 消除了調用服務時可能會有的猜測。
Swagger 的優勢
通過在項目中引入 Springfox,可以掃描相關的代碼,生成該描述文件,進而生成與代碼一致的接口文檔和客戶端代碼。
<!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-spring-web</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
![]()
在配置文件 config
目錄下,添加 swagger 的配置文件 SwaggerConfig.java
@Configuration // 配置類 @EnableSwagger2 // 開啟 swagger2 的自動配置 public class SwaggerConfig { }
這個時候 Swagger 已經算是整合到項目之中了,可以啟動下服務,輸入:http://localhost:8080/swagger-ui.html#
(這里我的項目端口是 8868 ,項目路徑是 /mike,所以我打開的文檔地址為 http://localhost:8868/mike/swagger-ui.html#
)即可查看 swagger 文檔。
可以看到 Swagger 文檔中大概有這四類信息
Swagger 有自己的實例 Docket,如果我們想要自定義基本信息,可以使用 docket 來配置 swagger 的基本信息,基本信息的設置在 ApiInfo
這個對象中。
Swagger 默認的基本信息展示
ApiInfo 中默認的基本設置
SwaggerConfig.java
配置文件添加以下內容:
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) // 配置基本信息 .apiInfo(apiInfo()) ; } // 基本信息設置 private ApiInfo apiInfo() { Contact contact = new Contact( "米大傻", // 作者姓名 "https://blog.csdn.net/xhmico?type=blog", // 作者網址 "777777777@163.com"); // 作者郵箱 return new ApiInfoBuilder() .title("多加辣-接口文檔") // 標題 .description("眾里尋他千百度,慕然回首那人卻在燈火闌珊處") // 描述 .termsOfServiceUrl("https://www.baidu.com") // 跳轉連接 .version("1.0") // 版本 .license("Swagger-的使用(詳細教程)") .licenseUrl("https://blog.csdn.net/xhmico/article/details/125353535") .contact(contact) .build(); }
![]()
重啟服務,打開 Swagger 文檔,基本信息改變如下所示:
默認情況下,Swagger 是會展示所有的接口信息的,包括最基礎的 basic-error
相關的接口
有時候我們希望不要展示 basic-error-controller
相關的接口,或者是說這想要顯示某些接口,比如說:user-controller
下的接口,由該怎么去實現呢?這個時候就需要設置 掃描接口
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors //.any() // 掃描全部的接口,默認 //.none() // 全部不掃描 .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 //.withClassAnnotation(RestController.class) // 掃描帶有指定注解的類下所有接口 //.withMethodAnnotation(PostMapping.class) // 掃描帶有只當注解的方法接口 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true //.none() // 不滿足條件的路徑,該斷言總為false(可用于生成環境屏蔽 swagger) //.ant("/user/**") // 滿足字符串表達式路徑 //.regex("") // 符合正則的路徑 ) .build(); }
![]()
可根據自己的需求去設置對應的配置,這里我就不再一一贅述了,以上是我所設置的配置,重啟服務,打開 Swagger 文檔,接口信息改變如下所示:
可以看到之前 basic-error-controller
相關的接口已經沒有了
Swagger 默認只有一個 default 分組選項,如果沒有設置,所有的接口都會顯示在 default
`分組下,如果功能模塊和接口數量一多,就會顯得比較凌亂,不方便查找和使用。
swagger 文檔中組名默認是 default
,可通過 .groupName(String )
@Bean public Docket docket() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" ; }
修改后:
如果需要配置多個組的話,就需要配置多個 docket() 方法
,這里我就簡單寫兩組,代碼如下:
/** * 展示 controller 包下所有的接口 */ @Bean public Docket docket1() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true ) .build(); } /** * 展示路徑為 /error 的所有接口(基礎接口) */ @Bean public Docket docket2() { // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("yank") // 修改組名為 "yank" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .any() // 掃描全部的接口,默認 ) .paths(PathSelectors .ant("/error") // 滿足字符串表達式路徑 ) .build(); }
![]()
重啟服務,打開 Swagger 文檔,接口信息改變如下所示:
組名為 mike
的文檔中只有 user-controller
相關的接口信息
組名為 yank
的文檔中只有 basic-error-controller
相關的接口信息
在開發或者測試環境下,我們開啟 swagger 會方便前端和后端的交互,但是如果在生產環境下也開啟 swagger 的話,是會將接口暴露出去的,有極大風險,如何讓 swagger 根據不同的環境來決定是否開啟?
這里我準備了四個項目的配置文件,dev
、test
、pro
三個環境的配置文件僅是端口上的不同
application.yml
內容如下,用于指定選擇的環境:
spring: profiles: active: dev
可以通過代碼判斷此時是在什么環境:dev
、test
、pro
,如果是在 pro
生產環境,則關閉 swagger。
/** * swagger 配置 * @param environment 環境 */ @Bean public Docket docket(Environment environment) { // 設置環境范圍 Profiles profiles = Profiles.of("dev","test"); // 如果在該環境返回內則返回:true,反之返回 false boolean flag = environment.acceptsProfiles(profiles); // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .enable(flag) // 是否開啟 swagger:true -> 開啟,false -> 關閉 ; }
![]()
在 application.yml
全局配置文件中環境指向 dev
時,是可以打開 swagger 的
如果我將 application.yml
全局配置文件中環境指向 pro
時,就不能打開 swagger 了,提示 Could not render e, see the console
之前有說 Swagger 會將接口請求或者相應的實體類信息展示在 Models
下的,比如我 UserController.java
下有一個接口如下所示:
@PostMapping(value = "/query-user-info") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }
它的請求體是 IdReq
,響應是 ResponseBean
,Models
展示這兩個實體類信息如下:
前端可通過看這個 Models
知道后端定義實體類的信息。
該注解是作用于類上面的,是用來描述類的一些基本信息的。
相關屬性:
value
:提供類的一個備用名,如果不設置,默認情況下將使用 class 類的名稱
譬如:這個為給 IdReq
這個類添加該注解
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id請求體") public class IdReq { private String id; }
可以看到這里的名字從 IdReq
變成 Id請求體
了
它的作用是添加和操作屬性模塊的數據。
該注解的使用詳情可參見博客:@ApiModelProperty注解的用法
這里我還是以 IdReq
類為例,為該類的屬性添加說明
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id請求體") public class IdReq { @ApiModelProperty("主鍵id") private String id; }
可以看到這里對該字段有一個備注說明。
該注解用來對某個方法/接口進行描述
該注解的使用詳情可參見博客:Swagger @ApiOperation 注解詳解
這里我以 UserController
下的接口為例:
@PostMapping(value = "/query-user-info") @ApiOperation(value = "根據id查詢用戶詳情") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }
可以看見該接口就多了對其的描述信息。
該注解使用在方法上或者參數上,字段說明,表示對參數的添加元數據(說明或者是否必填等)
相關屬性:
這里我以 UserController
下的接口為例:
@PostMapping(value = "/query-user-infos") @ApiOperation(value = "條件查詢用戶信息") public ResponseBean queryUserInfos( // name 用戶名稱 不必填 @ApiParam(value = "用戶名稱", required = false) @RequestParam(required = false) String name, // gender 用戶性別 必填 @ApiParam(value = "用戶性別", required = true) @RequestParam(required = true) GenderEnum gender ) { return ResponseBean.success(userService.queryUserInfos(name,gender)); }
這里會展示請求參數的備注信息,以及是否必填等。
使用 swagger 除了讓前后端交互變得方便,也讓接口的請求變得簡單,只需要填寫好請求所需要的參數信息,便可直接發起請求。
比如說接口 /user/query-user-info
點擊 Try it out
設置好請求所需的參數,點擊 Execute
執行
就能看到接口響應的結果了
接口 /user/query-user-infos
也差不多
有時候我們的接口是需要獲取請求頭信息的,這樣的話就還需要在 swagger 配置中添加請求頭的配置。
@Bean public Docket docket() { // 設置請求頭 List<Parameter> parameters = new ArrayList<>(); parameters.add(new ParameterBuilder() .name("token") // 字段名 .description("token") // 描述 .modelRef(new ModelRef("string")) // 數據類型 .parameterType("header") // 參數類型 .defaultValue("default value") // 默認值:可自己設置 .hidden(true) // 是否隱藏 .required(false) // 是否必須 .build()); // 創建一個 swagger 的 bean 實例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改組名為 "mike" // 配置接口信息 .select() // 設置掃描接口 // 配置如何掃描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 掃描指定包下的接口,最為常用 ) .paths(PathSelectors .any() // 滿足條件的路徑,該斷言總為true ) .build() // 添加請求頭參數 .globalOperationParameters(parameters); }
![]()
比如接口:
@GetMapping(value = "/get-token") @ApiOperation(value = "獲取請求頭中的token信息") public void getToken( @RequestHeader(value = "token",required = false) String token ) { // 直接獲取 token 信息 System.out.println("token = " + token); // 通過代碼獲取 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (servletRequestAttributes != null) { HttpServletRequest request = servletRequestAttributes.getRequest(); String header = request.getHeader("token"); System.err.println("header = " + header); } }
![]()
可以看到這個接口已經可以去設置請求頭了,調用接口
后端也能獲取到。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
項目 | 描述 |
---|---|
開發語言 | HTML、JavaScript、CSS |
庫 | dyCalendarJS、vanilla-tilt |
Edge | 108.0.1462.54 (正式版本) (64 位) |
該項目中需要使用到的庫有:
如果你在觀看本篇文章前并沒有對這兩個庫進行了解,歡迎移步至我的另外兩篇文章進行學習:
該項目文件中我已對代碼進行了注釋。如遇不懂的地方,請嘗試查看相關注釋。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>毛玻璃-傾斜-日歷</title> <!-- 導入自定義 CSS 文件 --> <link rel="stylesheet" href="./index.css"> <!-- 導入 dycalendar.css --> <link rel="stylesheet" href="../dycalendar.min.css"> </head> <body> <div id="calendar" class="dycalendar-container"></div> <!-- 導入 dycalendar.js --> <script src="../dycalendar.min.js"></script> <!-- 導入 vanilla-tilt.js --> <script src="../vanilla-tilt.js"></script> <script> // 繪制日歷 dycalendar.draw({ target: '#calendar', // 指定用于創建日歷的 HTML 容器 type: 'month', // 設置日歷的類型 prevnextbutton: 'show', // 顯示 "<" 及 ">" 按鈕 highlighttoday: true // 高亮顯示當前日期 }) // 為目標元素添加傾斜效果 VanillaTilt.init(document.querySelector('#calendar'), { target: '#calendar', // 指定需要添加傾斜效果的目標元素 scale: 0.8, // 鼠標懸停于目標元素上時,目標元素的放縮倍數 glare: true, // 是否設置反光效果 'max-glare': 0.6 // 設置反光效果的強度 }) </script> </body> </html>
![]()
*{ /* 去除元素默認的內外邊距 */ margin: 0px; padding: 0px; /* 設置邊框時將壓縮內容區域,而不會向外擴張。 也就是說,為某個元素設置邊框并不會改變其寬高。 */ box-sizing: border-box; } body{ /* 顯示區域的最小高度為顯示窗口的高度 */ min-height: 100vh; /* 設置該元素內部元素居中顯示 */ display: flex; justify-content: center; align-items: center; /* 設置該元素的背景顏色 */ background-color: #161623; } body::before{ /* 若需要正常使用偽元素,必須為其設置 content 屬性 */ content: ''; width: 400px; height: 400px; /* 設置顏色漸變效果 */ background: linear-gradient(#ffc107,#e91e63); /* 設置邊框圓角,當該屬性的值為 50% 時元素邊框將顯示為一個圓 */ border-radius: 50%; /* 為該元素設置絕對定位,阻止該元素遮擋日歷 (定位元素可以設置 z-index 來調節顯示順序, z-index 的值越高,顯示優先級越大)。 */ position: absolute; top: 10%; left: 20%; z-index: -1; } body::after{ content: ''; width: 300px; height: 300px; position: absolute; background: linear-gradient(#2196f3,#31ff38); border-radius: 50%; top: 45%; left: 55%; z-index: -1; } #calendar{ /* 設置日歷的寬高 */ width: 400px; height: 400px; color: #fff; /* 設置日歷的背景元素,為產生毛玻璃效果,這里將背景顏色設置為白色, 將透明度設置為 0.1(透明度的取值范圍為 0~1,取值越接近 1 ,顏色 越不透明)。 */ background-color: rgb(255, 255, 255, 0.1); /* 設置 blur 過濾器,該過濾器可以將背景模糊化,參數中的 像素值設定越高,顯示得越是模糊。 */ backdrop-filter: blur(50px); /* 分別設置日歷的四條邊框,使日歷顯示得更為立體 */ border-top: 1px solid rgb(255, 255, 255, 0.5); border-left: 1px solid rgb(255, 255, 255, 0.5); border-right: 1px solid rgb(255, 255, 255, 0.2); border-bottom: 1px solid rgb(255, 255, 255, 0.2); border-radius: 5px; /* 設置日歷的內邊距 */ padding: 0px 20px; /* 設置日歷周邊的陰影效果,box-shadow 接收的值(如下)分別為 陰影的 X 偏移量、陰影的 Y 偏移量、擴散半徑、陰影顏色。 */ box-shadow: 5px 10px 10px rgb(0, 0, 0, 0.1); } /* 這里存在許多在 HTML 文件中沒有看到的類名,這是因為這些標簽 是 dyCalendarJS 通過 JavaScript 動態創建的元素,如果有需要對 日歷中的某些元素的樣式進行改變,可以通過瀏覽器的 檢查 功能來查看 JavaScript 創建的元素并對其樣式進行適當的修改。 */ /* 有些元素需要通過修改傳遞給 dycalendar.draw() 的配置對象中的 部分屬性才能夠被發現。 */ /* 設置日歷的頭部部分的樣式 */ #calendar .dycalendar-header{ margin-top: 60px; font-size: 20px; } /* 設置日歷 "<" 及 ">" 按鈕的樣式,應用該樣式時請將 傳遞給 dycalendar.draw() 的配置對象中的 prevnextbutton 屬性的值設置為 true 。 */ #calendar .dycalendar-header .prev-btn, #calendar .dycalendar-header .next-btn{ width: 40px; height: 30px; background-color: rgb(255, 255, 255, 0.15); /* 設置文本對其方式及行高以使 ">" 及 "<" 居中顯示 */ text-align: center; line-height: 30px; /* 設置上下方向的外邊距為 0px,設置左右方向的外邊距為 5px */ margin: 0px 5px; } #calendar .dycalendar-body table{ width: 100%; height: 100%; margin-top: 50px; } /* tr:nth-child(1) 選擇 table 標簽中的第一個 tr 元素 */ /* 設置日歷中星期(星期幾)標識的樣式 */ #calendar .dycalendar-body table tr:nth-child(1) td{ background-color: rgb(255, 255, 255, 0.15); margin-bottom: 20px; } #calendar .dycalendar-body table td{ border-radius: 3px; /* 設置鼠標懸停時的指針樣式 */ cursor: pointer; } /* :hover 偽類選擇器用于設置鼠標懸停在指定元素時, 某個元素的樣式 */ #calendar .dycalendar-today-date, #calendar .dycalendar-body table td:hover{ color: #000; /* 使用 !important 提升該屬性在多個設置了該屬性的選擇器 中的權重 */ background-color: #fff !important; }
分享此文一切功德,皆悉回向給文章原作者及眾讀者. 免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
最近也是臨近期末了,各種的期末大作業,后臺管理也是很多地方需要用到的,為了方便大家能快速上手,快速搭建一個簡單的后臺管理,我花了兩天時間整理了一下
我會從0開始介紹,從數據庫的設計到前端頁面的引入最后到后端代碼的編寫,你只需要會一點前端的基礎和ssm的基礎就能快速上手搭建一個簡單的后臺管理
本次案例分兩篇文章教學:
(第一篇):數據表設計,前端框架引入和編寫前端頁面,搭建基本的springboot項目,引入前端到springboot項目中,在瀏覽器顯示
(第二篇):后端代碼的設計,這部分邏輯涉及的比較多,所以單獨放一篇出來講,代碼從0手敲講解,保證你能學會,完成增刪改查的功能
目錄
前言和環境介紹
無論是做app,網站,還是小程序,都少不了后臺管理
那么對于前端不是很會,后端也是只會一些的人來說,如何快速搭建一個簡單的后臺管理系統呢,哎別急,今天就來教大家簡單快速搭建一個后臺管理系統
首先,簡單介紹一下我的開發環境
工具 用處 H+ 前端框架,直觀的教程文檔,非常實用 SpringBoot 后端框架,簡單上手,搭建快 MySQL 數據庫 IDEA 非常強大的編譯器 Ajax 異步請求,前端向后端發送請求 thymeleaf 模板引擎,實時渲染頁面,基于HTML HBuild X 前端編譯器,用其他的也可以,看自己 好了,環境介紹完畢,我們先從前端界面做起
數據庫
一個后臺管理,肯定少不了數據,不然怎么叫后臺管理呢
這里我是用的是MySQL數據庫,當然你使用其他的也行,不過后面在SpringBoot中要做不同的配置
在MySQL中新建一個user數據庫,新建一個t_user表,字段如下,id,用戶名,昵稱,密碼(id記得設置為自增長模式)
![]()
H+前端框架
基本介紹
官網地址:H+ 后臺主題UI框架 - 主頁
![]()
H+是一個非常強大的前端開源框架,開箱即用,不需要過多的配置,里面有非常多組件,具體就不一一介紹了,有興趣的自己去看看
![]()
我們要做的是后臺管理,所以我們直接找到表格,可以看到有很多樣式選擇,我們選擇一個簡單點
這里為了方便快速搭建我選擇基本表格,當時你們可以根據自己喜歡來選擇
![]()
H+框架引入
打開HBuild X編譯器(你用其他的也可以,沒影響)
在左側空白處新建一個項目(快捷鍵ctrl+n),選擇基于HTML普通項目,添加項目名稱,選好路徑,點擊創建
![]()
一個基本的前端項目就創建好了
![]()
接著,將H+框架的css,js,font等靜態資源全部復制項目下
![]()
編寫后臺表格頁面
找到H+框架中基本表格的源碼,復制代碼到index.htm下
再對其進行一點修改,去掉右上角的工具欄,加上增刪改查的按鈕
![]()
修改之后的頁面如下,用到了H+框中的表格、表單、按鈕、字體圖標庫,就不詳細介紹了,有興趣可以自己看看前端代碼,我們著重講解js和后端的搭建
![]()
同時,我們還需要一個彈出框,當點擊添加和修改的時候會彈出一個表單框
我們在H+前端框架的表單中找到彈出框示例,復制代碼做點修改
![]()
修改后如下:
![]()
![]()
modal彈出框原理
HTML頁面代碼比較多,就不放上來了,底部Gitee倉庫完整的項目,主要講解一下上面這個modal彈出框怎么實現的就行
首先,每個modal彈出框都有唯一的標識ID屬性,這里我們有兩個,一個添加用戶,一個修改用戶(里面的表單代碼我沒放出來,比較多,文章底部Gitee倉庫我上傳了完整開源項目)
<!-- 添加用戶的彈出框 --> <div id="modal-form-add" class="modal fade" aria-hidden="true"> </div>
<!-- 修改用戶的彈出框 --> <div id="modal-form-update" class="modal fade" aria-hidden="true"> </div>我們為每個修改和刪除按鈕都添加一個類名標識(這里為什么不用ID,是因為ID只能唯一標識,添加按鈕可以用ID,但是修改和刪除不能用ID只能用class,因為有多個修改和刪除按鈕,添加按鈕只有一個)
![]()
我們在js文件夾下面新建一個myJS文件夾,存放自己編寫的js代碼,新建一個index.js,添加以下代碼
// 監聽添加按鈕事件(通過id屬性監聽) $('#addUserBtn').click(function() { // 添加按鈕被點擊之后,展示modal框 $('#modal-form-add').modal('show'); }) // 監聽修改按鈕事件(通過class屬性監聽) $('.updateUserBtn').click(function() { // 修改按鈕被點擊之后,展示modal框 $('#modal-form-update').modal('show'); })然后再index.html底部引入index.js即可
![]()
就可以實現點擊添加和修改按鈕會彈出modal表單框,簡單前端管理頁面就搭建完畢了,接下來是后端
搭建后端
基本介紹
SpringBoot是目前非常主流的后端框架,簡化新spring應用的初始搭建以及開發過程,搭建快,省去了編寫大量配置文件的過程
創建SpringBoot項目
打開IDEA,選擇spring Initializr,點擊下一步
![]()
添加主包名稱,java version版本選擇8,點擊下一步
![]()
在左側找到這五個依賴,勾選上,點擊下一步
![]()
添加項目名稱(默認為之前填寫的主包名稱),項目路徑自己選擇,點擊完成
![]()
右下角處選擇自動導入pom.xml依賴(沒有的話直接跳過)
![]()
一個基本的SpringBoot項目就創建完畢了
![]()
基本配置
在搭建項目之前,先做一些基本的配置,如數據源(連接MySQL)、static靜態資源映射(避免被攔截)、設置端口號(避免沖突)等
刪除原有的application.properties文件,新建一個application.yml文件(兩種都可以,yml文件方便一點),添加以下配置信息
![]()
spring: # 數據源,連接MySQL數據庫 datasource: url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true username: 數據庫用戶名 password: 數據庫密碼 driver-class-name: com.mysql.cj.jdbc.Driver # JPA配置,打印sql語句 jpa: show-sql: true properties: hibernate: format_sql: true # mvc配置,映射html頁面 mvc: static-path-pattern: /** view: prefix: / suffix: .html # thymeleaf模板引擎配置,設置編碼,false取消緩存 thymeleaf: encoding: UTF-8 cache: false server: # 修改啟動端口號 port: 8081 # 靜態資源映射路徑 web: resources: static-locations: classpath:/static/在項目目錄下新建一個config包(存在基本配置類),新建一個MyWebMVCConfig類,代碼如下
@Configuration public class MyWebMVCConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { System.out.println("==========靜態資源攔截!============"); //將所有/static/** 訪問都映射到classpath:/static/ 目錄下 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
引入前端
把剛剛在HBuildX寫好的前端頁面引入到IDEA中,首先復制除了html的其他所有文件到static文件夾下
![]()
接著復制index.html到templates文件夾下
![]()
然后打開index.html,在所有訪問靜態資源的鏈接前面加上/static/
![]()
測試項目(測試一下瀏覽器是否能顯示頁面)
想項目目錄下新建一個controller包,新建一個IndexCtroller類,添加以下代碼
![]()
@RestController public class IndexController { @RequestMapping(value = "/index") // 訪問路徑 public ModelAndView toIndex() { // 返回templates目錄下index.html ModelAndView view = new ModelAndView("index"); return view; } }點擊啟動項目,選哪個都可以,我一般選第二個debug模式啟動,方式debug調試
![]()
等待啟動完成之后,打開瀏覽器,輸入 localhost:8081/index,頁面成功顯示
![]()
第一篇文章到這里介紹完畢,第二篇正在火速撰寫中······
Gitee開源項目地址(本次項目源碼)
SpringBoot項目教學合集: CSDN中的所有SpringBoot項目開源,持續更新新項目、新教學文章
各大技術基礎教學、實戰項目開發教學
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目錄
把函數當作一個參數傳到另外一個函數中,當需要用這個函數是,再回調運行()這個函數.
回調函數是一段可執行的代碼段,它作為一個參數傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數)代碼。(作為參數傳遞到另外一個函數中,這個作為參數的函數就是回調函數)
理解:函數可以作為一個參數傳遞到另外一個函數中。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
function print(num) {
-
console.log(num);
-
}
-
-
add(1, 2, print); //3
-
</script>
分析:add(1, 2, print);中,函數print作為一個參數傳入到add函數中,但并不是馬上起作用,而是var sum = num1 + num2;運行完之后需要打印輸出sum的時候才會調用這個函數。(這個作為參數傳遞到另外一個函數中,這個作為參數的函數就是回調函數.
匿名回調函數:
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
add(1, 2, function (sum) {
-
console.log(sum); //=>3
-
});
-
</script>
1.不會立即執行
回調函數作為參數傳遞給一個函數的時候,傳遞的只是函數的定義并不會立即執行。和普通的函數一樣,回調函數在調用函數數中也要通過()
運算符調用才會執行。
2.回調函數是一個閉包
回調函數是一個閉包,也就是說它能訪問到其外層定義的變量。
3.執行前類型判斷
在執行回調函數前最好確認其是一個函數。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
//判定callback接收到的數據是一個函數
-
if (typeof callback === 'function') {
-
//callback是一個函數,才能當回調函數使用
-
callback(sum);
-
}
-
}
-
</script>
注意在回調函數調用時this的執行上下文并不是回調函數定義時的那個上下文,而是調用它的函數所在的上下文。
舉例:
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData(function(n){
-
console.log(this,1111); //window 1111
-
})
-
}
-
}
-
obj.tool();
-
</script>
分析:this指向是 離它最近的或者嵌套級別的 function/方法的調用者,這里離它最近的function是
function(n),會回到上面的callback()中,這時候調用者就不是obj而是window。
解決回調函數this指向的方法1:箭頭函數
回調函數(若回調函數是普通函數時)當參數傳入另外的函數時,若不知道這個函數內部怎么調用回調函數,就會出現回調函數中的this指向不明確的問題(就比如上面例子中this指向的不是obj而是window)。所以 把箭頭函數當回調函數,然后作為參數傳入另外的函數中就不會出現this指向不明的問題。
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData((n)=>{
-
this.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
分析:回調函數用箭頭函數寫之后,this指向很明確,就是 離它最近的或者嵌套級別的 function/方法的調用者,所以這里是 obj 。
解決回調函數this指向的方法2:var self = this;
-
<script>
-
function createData(callback){
-
callback(999);
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
var self = this; //這里的this指向obj,然后當一個變量取用
-
createData(function(n){
-
self.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
有一個非常重要的原因 —— JavaScript 是事件驅動的語言。這意味著,JavaScript 不會因為要等待一個響應而停止當前運行,而是在監聽其他事件時繼續執行。來看一個基本的例子:
-
<script>
-
function first() {
-
console.log(1);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:正如你所料,first
函數首先被執行,隨后 second
被執行 —— 控制臺輸出:1 2
但如果函數 first
包含某種不能立即執行的代碼會如何呢?例如我們必須發送請求然后等待響應的 API 請求?為了模擬這種狀況,我們將使用 setTimeout
,它是一個在一段時間之后調用函數的 JavaScript 函數。我們將函數延遲 500 毫秒來模擬一個 API 請求,新代碼長這樣:
-
<script>
-
function first() {
-
// 模擬代碼延遲
-
setTimeout(function () { //所以function(){console.log(1)}是回調函數
-
console.log(1);
-
}, 500);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:這里 function(){console.log(1)}函數當作一個參數傳入setTimeout函數中,因為setTimeout是官方提供得一個函數,里面有很多復雜的業務程序,所以函數 function(){console.log(1)}傳入后,不一定馬上運行,要setTimeout里面要運行到function(){console.log(1)}時才會運行該函數參數,那是不是整個程序就一直等setTimeout運行?不是的?。?!
整個程序運行結果為: 2 1 ,并不是原先的1 2 .即使我們首先調用了 first()
函數,我們記錄的輸出結果卻在 second()
函數之后。
這不是 JavaScript 沒有按照我們想要的順序執行函數的問題,而是 JavaScript 在繼續向下執行 second()
之前沒有等待 first()
響應的問題。回調正是確保一段代碼執行完畢之后再執行另一段代碼的方式。
定義:回調函數被認為是一種高級函數,一種被作為參數傳遞給另一個函數的高級函數?;卣{函數的本質是一種模式(一種解決常見問題的模式),因此回調函數也被稱為回調模式。
簡而言之:一個函數在另一個函數中被調用。而且可以當參數傳給其他函數。
所以: 回調函數和異步操作的關系是沒有關系?。?!
那為什么很多的異步操作都有回填函數????
問:你所知道的異步操作,是回調的作用么??? 并不是。
回調:更多的可以理解為一種業務邏輯把 異步編程:JS代碼的執行順序
簡單理解:callback 顧名思義 打電話回來的意思
eg1:你點外賣,剛好你要吃的食物沒有了,于是你在店老板那里留下了你的電話,過了幾天店里有了,店員就打了你的電話,然后你接到電話后就跑到店里買了。在這個例子里,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店里后來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店里去取貨叫做響應回調事件。
eg2:再比如,你發送一個axios 請求,請求成功之后,觸發成功的回調函數,請求失敗觸發失敗的回調函數。這里面的回調函數更像是一個工具,后臺通過這個工具告訴你,你成功了抑或是失敗了。這里面的所有異步操作都和回調沒關系,真正的異步是then方法。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
概述
:基于WebGL
的三維引擎,目前是國內資料最多、使用最廣泛的三維引擎
,可以制作一些3D
可視化項目
目前隨著元宇宙
概念的爆火,THREE
技術已經深入到了物聯網、VR、游戲、數據可視化等多個平臺,今天我們主要基于THREE
實現一個三維的VR
看房小項目
Three.js
一般分為三個部分:場景、相機、渲染器,這三個主要的分支就構成了THREE.JS
的主要功能區,這三大部分還有許多細小的分支,這些留到我們后續抽出一些章節專門講解一下。
工作流程
:場景——相機——渲染器
從實際生活
中拍照角度立方體網格模型和光照組成了一個虛擬的三維場景
,相機對象就像你生活中使用的相機一樣可以拍照,只不過一個是拍攝真實的景物
,一個是拍攝虛擬的景物。拍攝一個物體的時候相機的位置和角度需要設置,虛擬的相機還需要設置投影方式
,當你創建好一個三維場景,相機也設置好,就差一個動作“咔”,通過渲染器
就可以執行拍照動作。
概述
:場景主要由網絡模型與光照組成,網絡模型分為幾何體與材質
幾何體就像我們小時候學我們就知道點線面體四種概念,點動成線,線動成面,面動成體
,而材質就像是是幾何體上面的涂鴉,有不同的顏色、圖案…
例子如下:
//打造酷炫三角形 for (let i = 0; i < 50; i++) { const geometry = new THREE.BufferGeometry(); const arr = new Float32Array(9); for (let j = 0; j < 9; j++) { arr[j] = Math.random() * 5; } geometry.setAttribute('position', new THREE.BufferAttribute(arr, 3)); let randomColor = new THREE.Color(Math.random(), Math.random(), Math.random()); const material = new THREE.MeshBasicMaterial({ color: randomColor, transparent: true, opacity:0.5, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); }
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UlBSgxKr-1666681292595)(https://gitee.com/riskbaby/picgo/raw/master/blog/202209211037215.png#pic_center)]
const geometry = new THREE.BoxGeometry(100, 100, 100); const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const cube = new THREE.Mesh(geometry, material); scene.add(cube);
const geometry = new THREE.ConeGeometry(5, 15, 32);//底面半徑 高 側邊三角分段 const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const clone = new THREE.Mesh(geometry, material); scene.add(clone);
概念
:光照對three.js
的物體全表面進行光照測試,有可能會發生光照融合
//環境光 const ambient = new THREE.AmbientLight(0x404040); scene.add(ambient);
概念
:向特定方向發射的光,太陽光
也視作平行的一種,和上面比較,物體變亮了
//平行光 顏色 強度 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(100, 100, 100);//光源位置 directionalLight.target = cube;//光源目標 默認 0 0 0 scene.add(directionalLight);
概念
:由中間向四周發射光、強度比平行光小
// 顏色 強度 距離 衰退量(默認1) const pointLight = new THREE.PointLight(0xff0000, 1, 100, 1); pointLight.position.set(50, 50, 50); scene.add(pointLight);
概念
:家里面的節能燈泡,強度較好
//聚光燈 const spotLigth = new THREE.PointLight(0xffffff); spotLigth.position.set(50, 50, 50); spotLigth.target = cube; spotLigth.angle = Math.PI / 6; scene.add(spotLigth);
概念
:光源直接放置于場景之上,光照顏色從天空光線顏色漸變到地面光線顏色
//半球光 const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);//天空 場景 scene.add(light);
參數(屬性) | 含義 |
---|---|
left | 渲染空間的左邊界 |
right | 渲染空間的右邊界 |
top | 渲染空間的上邊界 |
bottom | 渲染空間的下邊界 |
near | near屬性表示的是從距離相機多遠的位置開始渲染,一般情況會設置一個很小的值。 默認值0.1 |
far | far屬性表示的是距離相機多遠的位置截止渲染,如果設置的值偏小小,會有部分場景看不到。 默認值1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.OrthographicCamera(width / - 2, width / 2, height / 2, height / - 2, 1, 1000); scene.add(camera); camera.position.set(100, 200, 100);
參數 | 含義 | 默認值 |
---|---|---|
fov | fov表示視場,所謂視場就是能夠看到的角度范圍,人的眼睛大約能夠看到180度的視場,視角大小設置要根據具體應用,一般游戲會設置60~90度 | 45 |
aspect | aspect表示渲染窗口的長寬比,如果一個網頁上只有一個全屏的canvas畫布且畫布上只有一個窗口,那么aspect的值就是網頁窗口客戶區的寬高比 | window.innerWidth/window.innerHeight |
near | near屬性表示的是從距離相機多遠的位置開始渲染,一般情況會設置一個很小的值。 | 0.1 |
far | far屬性表示的是距離相機多遠的位置截止渲染,如果設置的值偏小,會有部分場景看不到 | 1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000); camera.position.set(150, 100, 300); camera.lookAt(scene.position);
概述
:從WEBGL
的角度來看,three
就是對它的進一步封裝,想要進一步了解渲染器
這方面的知識點還需要了解一下WEBGL
,這里我們就不做過多介紹了。
概述
:這部分對于我們是否能夠給別人呈現一個真實的渲染場景
來說,很重要,比如下面一個普普通通的正方體,我們只要一加上貼圖,立馬不一樣了。
以前
之后
概述
:目前有許許多多的貼圖
,比如基礎、透明、環境、法線、金屬、粗糙、置換等等,今天我們呢主要講解一下環境
和一點 HDR處理
在THREE
的世界里面,坐標抽x、y、z
的位置關系圖如下所示:
紅、綠、藍
分別代表x、z、y
,我們的貼圖就是在px nx py ny pz nz
這六個方向防止一張圖片,其中p就代表坐標軸的正方向
CubeTextureLoader
:加載CubeTexture
的一個類。 內部使用ImageLoader
來加載文件。
//場景貼圖 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //場景添加背景 scene.background = envTexture; //場景的物體添加環境貼圖(無默認情況使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//設置粗糙程度 metalness: 1,//金屬度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere);
gif
圖片有點大上傳不了,我就截了幾張圖
概述
:高動態范圍圖像,相比普通的圖像,能夠提供更多的動態范圍和圖像細節,一般被運用于電視顯示產品以及圖片視頻拍攝制作當中。
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader; const rgbeLoader = new RGBELoader().setPath('./textures/course/hdr/'); //異步加載 rgbeLoader.loadAsync('002.hdr').then((texture) => { //設置加載方式 等距圓柱投影的環境貼圖 texture.mapping = THREE.EquirectangularReflectionMapping; scene.background = texture; })
概述
:坐標軸能夠更好的反饋物體的位置信息,紅、綠、藍
分別代表x、z、y
const axesHelper = new THREE.AxesHelper(20);//里面的數字代表坐標抽長度 scene.add(axesHelper);
概述
:通過鼠標控制物體和相機的移動、旋轉、縮放
導包
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
應用
const controls = new OrbitControls(camera, renderer.domElement)
自旋轉
controls.autoRotate = true
必須在render
函數調用update
實時更新才奏效
概述
:根據屏幕大小自適應場景
//自適應屏幕 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(window.devicePixelRatio) })
設置相機的寬高比、重新更新渲染相機、渲染器的渲染大小、設備的像素比
概述
:雙擊進入全屏
,再次雙擊/ESC退出全屏
window.addEventListener('dblclick', () => { let isFullScreen = document.fullscreenElement if (!isFullScreen) { renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } })
概述
;通過操作面板完成界面的移動物體
的相關應用
鏈接
:https://www.npmjs.com/package/dat.gui
//安裝npm npm install --save dat.gui //如果出現...標記錯誤,安裝到開發依賴就可以了 npm i --save-dev @types/dat.gui
//界面操作 const gui = new dat.GUI(); //操作物體位置 gui .add(cube.position, 'x') .min(0) .max(10) .step(0.1) .name('X軸移動') .onChange((value) => { console.log('修改的值為' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物體顏色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物體顏色 cube.material.color.set(value); });
概述
:檢測幀率
導包
import Stats from 'three/addons/libs/stats.module.js';
應用
const stats = new Stats(); document.body.appendChild(stats.dom);
自變化
stats.update()
必須在render
函數調用update
實時更新才奏效
概述
:底部二維平面的網格化,幫助我們更好的創建場景
const gridHelper = new THREE.GridHelper(10, 20)//網格大小、細分次數 scene.add(gridHelper)
//導入包 import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import * as dat from 'dat.gui'; import Stats from 'three/addons/libs/stats.module.js'; let scene,camera,renderer //場景 scene = new THREE.Scene(); //坐標抽 const axesHelper = new THREE.AxesHelper(20); scene.add(axesHelper); //場景貼圖 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //場景添加背景 scene.background = envTexture; //場景的物體添加環境貼圖(無默認情況使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//設置粗糙程度 metalness: 1,//金屬度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere); //光照 const ambient = new THREE.AmbientLight(0xffffff); scene.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.05); directionalLight.position.set(10,10,10); directionalLight.lookAt(scene.position); scene.add( directionalLight ); //相機 camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 2000, ); camera.position.set(10,10,20); camera.lookAt(scene.position); scene.add(camera); //渲染器 renderer = new THREE.WebGLRenderer({ //防止鋸齒 antialias: true, }); renderer.setSize(window.innerWidth, window.innerHeight); // renderer.setClearColor(0xb9d3ff, 1); document.body.appendChild(renderer.domElement); //鼠標控制器 const controls = new OrbitControls(camera, renderer.domElement); //阻尼 必須在 render函數調用 controls.update(); controls.dampingFactor = true; controls.autoRotate=true const stats=new Stats() document.body.appendChild(stats.dom); function render () { renderer.render(scene, camera); requestAnimationFrame(render); controls.update();//調用 stats.update() } render(); //全屏操作 window.addEventListener('dblclick', () => { //查詢是否全屏 let isFullScene = document.fullscreenElement; console.log(isFullScene); if (!isFullScene) { renderer.domElement.requestFullscreen(); } else { document.exitFullscreen(); } }) //自適應 window.addEventListener('resize', () => { //寬高比 camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio);//設置像素比 }) //界面操作 const gui = new dat.GUI(); //操作物體位置 gui .add(sphere.position, 'x') .min(0) .max(10) .step(0.1) .name('X軸移動') .onChange((value) => { console.log('修改的值為' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物體顏色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物體顏色 sphere.material.color.set(value); });
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目錄
相同點:
- 都是基于tcp的,都是可靠性傳輸協議
- 都是應用層協議
不同點:
- WebSocket是雙向通信協議,模擬Socket協議,可以雙向發送或接受信息
- HTTP是單向的
- WebSocket是需要瀏覽器和服務器握手進行建立連接的
- 而http是瀏覽器發起向服務器的連接,服務器預先并不知道這個連接
聯系:
- WebSocket在建立握手時,數據是通過HTTP傳輸的。但是建立之后,在真正傳輸時候是不需要HTTP協議的
總結(總體過程):
- 首先,客戶端發起http請求,經過3次握手后,建立起TCP連接;http請求里存放WebSocket支持的版本號等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服務器收到客戶端的握手請求后,同樣采用HTTP協議回饋數據;
- 最后,客戶端收到連接成功的消息后,開始借助于TCP傳輸信道進行全雙工通信。
從例子上來看有個問題:
- 假如有好多人一起在快遞站等快遞,那么這個地方是否足夠大,(抽象解釋:需要有很高的并發,同時有很多請求等待在這里)
推送延遲。服務端數據發生變更后,長輪詢結束,立刻返回響應給客戶端。
服務端壓力。長輪詢的間隔期一般很長,例如 30s、60s,并且服務端 hold 住連接不會消耗太多服務端資源。
從例子上來看有兩個問題:
- 假如說,張三打電話的時間間隔為10分鐘,當他收到快遞前最后一次打電話,快遞員說沒到,他剛掛掉電話,快遞入庫了(就是到了),那么等下一次時間到了,張三打電話知道快遞到了,那么這樣的通訊算不算實時通訊?很顯然,不算,中間有十分鐘的時間差,還不算給快遞員打電話的等待時間(抽象的解釋:每次request的請求時間間隔等同于十分鐘,請求解析相當于等待)
- 假如說張三所在的小區每天要收很多快遞,每個人都采取主動給快遞員打電話的方式,那么快遞員需要以多快的速度接到,其他人打電話占線也是問題(抽象解釋:請求過多,服務端響應也會變慢)
推送延遲。
服務端壓力。配置一般不會發生變化,頻繁的輪詢會給服務端造成很大的壓力。
推送延遲和服務端壓力無法中和。降低輪詢的間隔,延遲降低,壓力增加;增加輪詢的間隔,壓力降低,延遲增高
一旦WebSocket連接建立后,后續數據都以幀序列的形式傳輸。在客戶端斷開WebSocket連接或Server端中斷連接前,不需要客戶端和服務端重新發起連接請求。在海量并發及客戶端與服務器交互負載流量大的情況下,極大的節省了網絡帶寬資源的消耗,有明顯的性能優勢,且客戶端發送和接受消息是在同一個持久連接上發起,實現了“真·長鏈接”,實時性優勢明顯。
WebSocket有以下特點:
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
目前主流的 Web 開發模式有兩種,分別是:
服務端渲染的概念:服務器發送給客戶端的 HTML 頁面,是在服務器通過字符串的拼接,動態生成的。因此,客戶端不需要使用 Ajax 這樣的技術額外請求頁面的數據。代碼示例如下:
優點:
缺點:
前后端分離的概念:前后端分離的開發模式,依賴于 Ajax 技術的廣泛應用。簡而言之,前后端分離的 Web 開發模式,就是后端只負責提供 API 接口,前端使用 Ajax 調用接口的開發模式。
優點:
缺點:
不利于 SEO。因為完整的 HTML 頁面需要在客戶端動態拼接完成,所以爬蟲對無法爬取頁面的有效信息。(解決方案:利用 Vue、React 等前端框架的 SSR (server side render)技術能夠很好的解決 SEO 問題?。?/span>
不談業務場景而盲目選擇使用何種開發模式都是耍流氓。
另外,具體使用何種開發模式并不是絕對的,為了同時兼顧了首頁的渲染速度和前后端分離的開發效率,一些網站采用了首屏服務器端渲染 + 其他頁面前后端分離的開發模式。
身份認證(Authentication)又稱“身份驗證”、“鑒權”,是指通過一定的手段,完成對用戶身份的確認。
身份認證的目的,是為了確認當前所聲稱為某種身份的用戶,確實是所聲稱的用戶。例如,你去找快遞員取快遞,你要怎么證明這份快遞是你的。
在互聯網項目開發中,如何對用戶的身份進行認證,是一個值得深入探討的問題。例如,如何才能保證網站不會錯誤的將“馬云的存款數額”顯示到“馬化騰的賬戶”上。
對于服務端渲染和前后端分離這兩種開發模式來說,分別有著不同的身份認證方案:
對于超市來說,為了方便收銀員在進行結算時給 VIP 用戶打折,超市可以為每個 VIP 用戶發放會員卡。
注意:現實生活中的會員卡身份認證方式,在 Web 開發中的專業術語叫做 Cookie
Cookie的幾大特性:
客戶端第一次請求服務器的時候,服務器通過響應頭的形式,向客戶端發送一個身份認證的 Cookie,客戶端會自動將 Cookie 保存在瀏覽器中。
隨后,當客戶端瀏覽器每次請求服務器的時候,瀏覽器會自動將身份認證相關的 Cookie,通過請求頭的形式發送給服務器,服務器即可驗明客戶端的身份。
由于 Cookie 是存儲在瀏覽器中的,而且瀏覽器也提供了讀寫 Cookie 的 API,因此 Cookie 很容易被偽造,不具有安全性。因此不建議服務器將重要的隱私數據,通過 Cookie 的形式發送給瀏覽器。
注意:千萬不要使用 Cookie 存儲重要且隱私的數據!比如用戶的身份信息、密碼等。
3.6、提高身份認證的安全性
為了防止客戶偽造會員卡,收銀員在拿到客戶出示的會員卡之后,可以在收銀機上進行刷卡認證。只有收銀機確認存在的會員卡,才能被正常使用。
這種“會員卡 + 刷卡認證”的設計理念,就是 Session 認證機制的精髓。
在 Express 項目中,只需要安裝 express-session 中間件,即可在項目中使用 Session 認證:
npm install express-session
express-session 中間件安裝成功后,需要通過 app.use() 來注冊 session 中間件,示例代碼如下:
-
// 導入 session 中間件
-
const session = require('express-session')
-
-
// 配置 session 中間件
-
app.use(
-
session({
-
secret: 'itheima', // secret 屬性的值可以為任意字符串
-
resave: false, // 固定寫法
-
saveUninitialized: true, // 固定寫法
-
})
-
)
當 express-session 中間件配置成功后,即可通過 req.session 來訪問和使用 session 對象,從而存儲用戶的關鍵信息:
-
// 登錄的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判斷用戶提交的登錄信息是否正確
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登錄失敗' })
-
}
-
-
// TODO_02:請將登錄成功后的用戶信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 這個中間件之后,才能夠通過 req 點出來 session 這個屬性
-
req.session.user = req.body // 用戶的信息
-
console.log(req.body)
-
req.session.islogin = true // 用戶的登錄狀態
-
-
res.send({ status: 0, msg: '登錄成功' })
-
})
可以直接從 req.session 對象上獲取之前存儲的數據,示例代碼如下:
-
// 獲取用戶姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:請從 Session 中獲取用戶的名稱,響應給客戶端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
調用 req.session.destroy() 函數,即可清空服務器保存的 session 信息。
-
// 退出登錄的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登錄成功',
-
})
-
})
index.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>后臺主頁</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<h1>首頁</h1>
-
-
<button id="btnLogout">退出登錄</button>
-
-
<script>
-
$(function () {
-
-
// 頁面加載完成后,自動發起請求,獲取用戶姓名
-
$.get('/api/username', function (res) {
-
// status 為 0 表示獲取用戶名稱成功;否則表示獲取用戶名稱失敗!
-
if (res.status !== 0) {
-
alert('您尚未登錄,請登錄后再執行此操作!')
-
location.href = './login.html'
-
} else {
-
alert('歡迎您:' + res.username)
-
}
-
})
-
-
// 點擊按鈕退出登錄
-
$('#btnLogout').on('click', function () {
-
// 發起 POST 請求,退出登錄
-
$.post('/api/logout', function (res) {
-
if (res.status === 0) {
-
// 如果 status 為 0,則表示退出成功,重新跳轉到登錄頁面
-
location.href = './login.html'
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
login.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>登錄頁面</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<!-- 登錄表單 -->
-
<form id="form1">
-
<div>賬號:<input type="text" name="username" autocomplete="off" /></div>
-
<div>密碼:<input type="password" name="password" /></div>
-
<button>登錄</button>
-
</form>
-
-
<script>
-
$(function () {
-
// 監聽表單的提交事件
-
$('#form1').on('submit', function (e) {
-
// 阻止默認提交行為
-
e.preventDefault()
-
// 發起 POST 登錄請求
-
$.post('/api/login', $(this).serialize(), function (res) {
-
// status 為 0 表示登錄成功;否則表示登錄失??!
-
if (res.status === 0) {
-
location.href = './index.html'
-
} else {
-
alert('登錄失?。?)
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
app.js
-
// 導入 express 模塊
-
const express = require('express')
-
// 創建 express 的服務器實例
-
const app = express()
-
-
// TODO_01:請配置 Session 中間件
-
const session = require('express-session')
-
app.use(
-
session({
-
secret: 'itheima',
-
resave: false,
-
saveUninitialized: true,
-
})
-
)
-
-
// 托管靜態頁面
-
app.use(express.static('./pages'))
-
// 解析 POST 提交過來的表單數據
-
app.use(express.urlencoded({ extended: false }))
-
-
// 登錄的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判斷用戶提交的登錄信息是否正確
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登錄失敗' })
-
}
-
-
// TODO_02:請將登錄成功后的用戶信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 這個中間件之后,才能夠通過 req 點出來 session 這個屬性
-
req.session.user = req.body // 用戶的信息
-
console.log(req.body)
-
req.session.islogin = true // 用戶的登錄狀態
-
-
res.send({ status: 0, msg: '登錄成功' })
-
})
-
-
// 獲取用戶姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:請從 Session 中獲取用戶的名稱,響應給客戶端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
-
-
// 退出登錄的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登錄成功',
-
})
-
})
-
-
// 調用 app.listen 方法,指定端口號并啟動web服務器
-
app.listen(80, function () {
-
console.log('Express server running at http://127.0.0.1:80')
-
})
Session 認證機制需要配合 Cookie 才能實現。由于 Cookie 默認不支持跨域訪問,所以,當涉及到前端跨域請求后端接口的時候,需要做很多額外的配置,才能實現跨域 Session 認證。
注意:
JWT(英文全稱:JSON Web Token)是目前最流行的跨域認證解決方案。
總結:用戶的信息通過 Token 字符串的形式,保存在客戶端瀏覽器中。服務器通過還原 Token 字符串的形式來認證用戶的身份。
JWT 通常由三部分組成,分別是 Header(頭部)、Payload(有效荷載)、Signature(簽名)。
三者之間使用英文的“.”分隔,格式如下:
下面是 JWT 字符串的示例:
JWT 的三個組成部分,從前到后分別是 Header、Payload、Signature。
其中:
客戶端收到服務器返回的 JWT 之后,通常會將它儲存在 localStorage 或 sessionStorage 中。
此后,客戶端每次與服務器通信,都要帶上這個 JWT 的字符串,從而進行身份認證。推薦的做法是把 JWT 放在 HTTP 請求頭的 Authorization 字段中,格式如下:
運行如下命令,安裝如下兩個 JWT 相關的包:
npm install jsonwebtoken express-jwt
其中:
使用 require() 函數,分別導入 JWT 相關的兩個包:
-
// 安裝并導入 JWT 相關的兩個包,分別是 jsonwebtoken 和 express-jwt
-
const jwt = require('jsonwebtoken')
-
const expressJWT = require('express-jwt')
為了保證 JWT 字符串的安全性,防止 JWT 字符串在網絡傳輸過程中被別人破解,我們需要專門定義一個用于加密和解密的 secret 密鑰:
-
// 定義 secret 密鑰,建議將密鑰命名為 secretKey,本質上就是一個字符串
-
const secretKey = 'itheima No1 ^_^'
調用 jsonwebtoken 包提供的 sign() 方法,將用戶的信息加密成 JWT 字符串,響應給客戶端:
-
// 登錄接口
-
app.post('/api/login', function (req, res) {
-
// 將 req.body 請求體中的數據,轉存為 userinfo 常量
-
const userinfo = req.body
-
// 登錄失敗
-
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
-
return res.send({
-
status: 400,
-
message: '登錄失?。?,
-
})
-
}
-
// 登錄成功
-
// TODO_03:在登錄成功之后,調用 jwt.sign() 方法生成 JWT 字符串。并通過 token 屬性發送給客戶端
-
// 參數1:用戶的信息對象
-
// 參數2:加密的秘鑰
-
// 參數3:配置對象,可以配置當前 token 的有效期
-
// 記?。呵f不要把密碼加密到 token 字符中
-
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
-
res.send({
-
status: 200,
-
message: '登錄成功!',
-
token: tokenStr, // 要發送給客戶端的 token 字符串
-
})
-
})
-
// 使用 app.use() 來注冊中間件
-
// expressJWT({ secret: secretKey }) 就是用來解析 Token 的中間件
-
// .unless({ path: [/^\/api\//] }) 用來指定哪些接口不需要訪問權限
-
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
6.6、使用 req.user 獲取用戶信息
當 express-jwt 這個中間件配置成功之后,即可在那些有權限的接口中,使用 req.user 對象,來訪問從 JWT 字符串中解析出來的用戶信息了,示例代碼如下:
-
// 這是一個有權限的 API 接口
-
app.get('/admin/getinfo', function (req, res) {
-
// TODO_05:使用 req.user 獲取用戶信息,并使用 data 屬性將用戶信息發送給客戶端
-
console.log(req.user)
-
res.send({
-
status: 200,
-
message: '獲取用戶信息成功!',
-
data: req.user, // 要發送給客戶端的用戶信息
-
})
-
})
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
jsoup 是一款基于 Java 的HTML解析器,它提供了一套非常省力的API,不但能直接解析某個URL地址、HTML文本內容,而且還能通過類似于DOM、CSS或者jQuery的方法來操作數據,所以 jsoup 也可以被當做爬蟲工具使用。
Document
:文檔對象。每份HTML頁面都是一個文檔對象,Document 是 jsoup 體系中最頂層的結構。
Element
:元素對象。一個 Document 中可以著包含著多個 Element 對象,可以使用 Element 對象來遍歷節點提取數據或者直接操作HTML。
Elements
:元素對象集合,類似于List<Element>
。
Node
:節點對象。標簽名稱、屬性等都是節點對象,節點對象用來存儲數據。
獲得文檔對象 Document 一共有4種方法,分別對應不同的獲取方式。
正式開始之前,我們需要導入有關 jar 包。
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.1</version> </dependency>
–
使用 Jsoup.connect(String url).get()
方法獲取(只支持 http 和 https 協議):
Document doc = Jsoup.connect("http://csdn.com/").get(); String title = doc.title(); System.out.println(title);
connect(String url)
方法創建一個新的 Connection并通過.get()
或者.post()
方法獲得數據。如果從該URL獲取HTML時發生錯誤,便會拋出 IOException,應適當處理。
Connection
接口還提供一個方法鏈來解決特殊請求,我們可以在發送請求時帶上請求的頭部參數,具體如下:
Document doc = Jsoup.connect("http://csdn.com") .data("query", "Java") .userAgent("Mozilla") .cookie("auth", "token") .timeout(8000) .post(); System.out.println(doc);
想獲得完整的響應對象和響應碼?我們可以使用execute()
方法:
// 獲得響應對象 Connection.Response response = Jsoup.connect("http://csdn.com").execute(); int code = response.statusCode(); // 輸出狀態碼:200 System.out.println(code);
–
可以使用靜態的Jsoup.parse(File in, String charsetName)
方法從文件中加載文檔。其中in
表示路徑,charsetName
表示編碼方式,示例代碼:
File input = new File("/tmp/input.html"); Document doc = Jsoup.parse(input, "UTF-8"); System.out.println(doc);
–
使用靜態的Jsoup.parse(String html)
方法可以從字符串文本中獲得文檔對象 Document ,示例代碼:
String html = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>"; Document doc = Jsoup.parse(html); System.out.println(doc);
–
使用Jsoup.parseBodyFragment(String html)
方法.
String html = "<p>Lorem ipsum.</p>"; Document doc = Jsoup.parseBodyFragment(html); // doc 此時為:<body> <p>Lorem ipsum.</p></body> Element body = doc.body(); System.out.println(body);
parseBodyFragment
方法創建一個新的文檔,并插入解析過的HTML到body
元素中。假如你使用正常的 Jsoup.parse(String html)
方法,通常也能得到相同的結果,但是明確將用戶輸入作為 body 片段處理是個更好的方式。
Document.body()
方法能夠取得文檔body元素的所有子元素,與 doc.getElementsByTag("body")
相同。
解析文檔對象并獲取數據一共有 2 種方式,分別為 DOM方式、CSS選擇器方式,我們可以選擇一種自己喜歡的方式去獲取數據,效果一樣。
將HTML解析成一個Document
之后,就可以使用類似于DOM的方法進行操作。
// 獲取csdn首頁所有的鏈接 Document doc = Jsoup.connect("http://csdn.com").get(); Elements elements = doc.getElementsByTag("body"); Elements contents = elements.first().getElementsByTag("a"); for (Element content : contents) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText+"\t"); System.out.println(linkHref); }
說明
Elements
這個對象提供了一系列類似于DOM的方法來查找元素,抽取并處理其中的數據。具體如下:
4.1.1)查找元素
getElementById(String id)
:通過id來查找元素
getElementsByTag(String tag)
:通過標簽來查找元素
getElementsByClass(String className)
:通過類選擇器來查找元素
getElementsByAttribute(String key)
:通過屬性名稱來查找元素,例如查找帶有href元素的標簽。
siblingElements()
:獲取兄弟元素。如果元素沒有兄弟元素,則返回一個空列表。
firstElementSibling()
:獲取第一個兄弟元素。
lastElementSibling()
:獲取最后一個兄弟元素。
nextElementSibling()
:獲取下一個兄弟元素。
previousElementSibling()
:獲取上一個兄弟元素。
parent()
:獲取此節點的父節點。
children()
:獲取此節點的所有子節點。
child(int index)
:獲取此節點的指定子節點。
4.1.2)獲取元素數據
attr(String key)
:獲取單個屬性值
attributes()
:獲取所有屬性值
attr(String key, String value)
:設置屬性值
text()
:獲取文本內容
text(String value)
:設置文本內容
html()
:獲取元素內的HTML內容
html(String value)
:設置元素內的HTML內容
outerHtml()
:獲取元素外HTML內容
data()
:獲取數據內容(例如:script和style標簽)
id()
:獲得id值(例:<p id="goods">衣服</p>
)
className()
:獲得第一個類選擇器值
classNames()
:獲得所有的類選擇器值
tag()
:獲取元素標簽
tagName()
:獲取元素標簽名(如:<p>、<div>等)
4.1.3)操作HTML文本
append(String html)
:在末尾追加HTML文本
prepend(String html)
:在開頭追加HTML文本
html(String value)
:在匹配元素內部添加HTML文本。
–
可以使用類似于CSS選擇器的語法來查找和操作元素,常用的方法為select(String selector)
。
Document doc = Jsoup.connect("http://csdn.com").get(); // 獲取帶有 href 屬性的 a 元素 Elements elements = doc.select("a[href]"); for (Element content : elements) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText + "\t"); System.out.println(linkHref); }
4.2.1)說明
select()
方法在Document
、Element
或Elements
對象中都可以使用,而且是上下文相關的,因此可實現指定元素的過濾,或者采用鏈式訪問。
select()
方法將返回一個Elements
集合,并提供一組方法來抽取和處理結果。
4.2.2)select(String selector)
方法參數簡介
tagname
: 通過標簽查找元素,例如通過"a"
來查找<a>
標簽。
#id
: 通過ID查找元素,比如通過#logo
查找<p id="logo">
。
.class
: 通過class名稱查找元素,比如通過.titile
查找<p class="titile">
。
ns|tag
: 通過標簽在命名空間查找元素,比如使用 fb|name
來查找 <fb:name>
。
[attribute]
: 利用屬性查找元素,比如通過[href]
查找<a href="...">
。
[^attribute]
: 利用屬性名前綴來查找元素,比如:可以用[^data-]
來查找帶有HTML5 dataset屬性的元素。
[attribute=value]
: 利用屬性值來查找元素,比如:[width=500]
。
[attribute^=value]
, [attribute$=value]
, [attribute*=value]
: 利用匹配屬性值開頭、結尾或包含屬性值來查找元素,比如通過[href*=/path/]
來查找<a href="a/path/c.html">
。
[attribute~=regex]
: 利用屬性值匹配正則表達式來查找元素,比如通過 img[src~=(?i)\.(png|jpe?g)]
來匹配所有的png或者jpg、jpeg格式的圖片。
*
: 通配符,匹配所有元素。
4.2.3)參數屬性組合使用
el#id
: 元素+ID,比如: div#logo
el.class
: 元素+class,比如: div.masthead
el[attr]
: 元素+class,比如 a[href]
匹配所有帶有 href 屬性的 a 元素。
a[href].highlight
匹配所有帶有 href 屬性且class="highlight"的 a 元素。
ancestor child
: 查找某個元素下子元素,比如:可以用.body p
查找在"body"元素下的所有 p
元素
parent > child
: 查找某個父元素下的直接子元素,比如:可以用div.content > p
查找 p
元素,也可以用body > *
查找body標簽下所有直接子元素
siblingA + siblingB
: 查找在A元素之前第一個同級元素B,比如:div.head + div
siblingA ~ siblingX
: 查找A元素之前的同級X元素,比如:h1 ~ p
el, el, el
:多個選擇器組合,查找匹配任一選擇器的唯一元素,例如:div.masthead, div.logo
4.2.4)特殊參數:偽選擇器
:lt(n)
: 查找哪些元素的同級索引值(它的位置在DOM樹中是相對于它的父節點)小于n,比如:td:lt(3)
表示小于三列的元素
:gt(n)
:查找哪些元素的同級索引值大于n``,比如
: div p:gt(2)
表示哪些div中有包含2個以上的p元素
:eq(n)
: 查找哪些元素的同級索引值與n
相等,比如:form input:eq(1)
表示包含一個input標簽的Form元素
:has(seletor)
: 查找匹配選擇器包含元素的元素,比如:div:has(p)
表示哪些div包含了p元素
:not(selector)
: 查找與選擇器不匹配的元素,比如: div:not(.logo)
表示不包含 class=logo 元素的所有 div 列表
:contains(text)
: 查找包含給定文本的元素,搜索不區分大不寫,比如: p:contains(jsoup)
:containsOwn(text)
: 查找直接包含給定文本的元素
:matches(regex)
: 查找哪些元素的文本匹配指定的正則表達式,比如:div:matches((?i)login)
:matchesOwn(regex)
: 查找自身包含文本匹配指定正則表達式的元素
在獲得文檔對象并且指定查找元素后,我們就可以獲取元素中的數據。
這些訪問器方法都有相應的setter方法來更改數據。
.attr(String key)
:獲得屬性的值。
.text()
:獲得元素中的文本。
.html()
:獲得元素或屬性內部的HTML內容(不包括本身)。
.outerHtml()
:獲得元素或屬性完整的HTML內容。
.id()
:獲得元素id屬性值。
className()
:獲得元素類選擇器值。
.tagName()
:獲得元素標簽命名。
.hasClass(String className)
:檢查這個元素是否含有一個類選擇器(不區分大小寫)。
String html = "<p><a ><b>example</b></a> link.</p>"; Document doc = Jsoup.parse(html); // 查找第一個<a>元素 Element link = doc.select("a").first(); // 輸出:example String text = link.text(); // 輸出:http://csdn.com/ String href = link.attr("href"); // 輸出:<b>example</b> String aHtml = link.outerHtml(); // 輸出:<a ><b>example</b></a> String aOuterHtml = link.outerHtml();
–
在解析了一個Document對象之后,你可能想修改其中的某些屬性值,并把它輸出到前臺頁面或保存到其他地方,jsoup對此提供了一套非常簡便的接口(支持鏈式寫法)。
當以下方法針對Element
對象操作時,只有一個元素會受到影響。當針對Elements
對象進行操作時,可能會影響到多個元素。
.attr(String key, String value)
:設置標簽的屬性值。
.addClass(String className)
:增加類選擇器選項
.removeClass(String className)
:刪除對應的類選擇器
Document doc = Jsoup.connect("http://csdn.com").get(); // 復數,Elements Elements elements = doc.getElementsByClass("text"); // 單數,Element Element element = elements.first(); // 復數對象,所有 class="text" 的元素都將受到影響 elements.attr("name","goods"); // 單數對象,只有一個元素會受到影響(鏈式寫法) element.attr("name","shop") .addClass("red");
可以使用Element
中的HTML設置方法具體如下:
.html(String value)
:這個方法將先清除元素中的HTML內容,然后用傳入的HTML代替。
.prepend(String value)
:在元素前添加html內容。
.append(String value)
:在元素后添加html內容。
.wrap(String value)
:對元素包裹一個外部HTML內容,將元素置于新增的內容中間。
Document doc = Jsoup.connect("http://csdn.com").get(); Element div = doc.select("div").first(); div.html("<p>csdn</p>"); div.prepend("<p>a</p>"); div.append("<p>good</p>"); // 輸出:<div"> <p>a</p> <p>csdn</p> <p>good</p> </div> Element span = doc.select("span").first(); span.wrap("<li><a href='...'></a></li>"); // 輸出: <li><a href="..."> <span>csdn</span> </a></li>
對于傳入的文本,如果含有像 <
, >
等這樣的字符,將以文本處理,而非HTML。
.text(String text)
:清除元素內部的HTML內容,然后用提供的文本代替。
.prepend(String first)
:在元素后添加文本節點。
Element.append(String last)
:在元素前添加文本節點。
// <div></div> Element div = doc.select("div").first(); div.text(" one "); div.prepend(" two "); div.append(" three "); // 輸出: <div> two one three </div>
問題描述:
你有一個包含相對URLs路徑的HTML文檔,現在需要將這些相對路徑轉換成絕對路徑的URLs。
解決方式:
base URI
路徑。
abs:
屬性前綴來取得包含base URI
的絕對路徑。代碼如下:
Document doc = Jsoup.connect("http://www.open-open.com").get(); Element link = doc.select("a").first(); // 輸出:/ String relHref = link.attr("href"); // 輸出:http://www.open-open.com/ String absHref = link.attr("abs:href");
說明:
在HTML元素中,URLs經常寫成相對于文檔位置的相對路徑,如:<a href="/download">...</a>
。當你使用 .attr(String key)
方法來取得a元素的href屬性時,它將直接返回在HTML源碼中指定的值。
假如你需要取得一個絕對路徑,需要在屬性名前加 abs:
前綴,這樣就可以返回包含根路徑的URL地址attr("abs:href")
。因此在解析HTML文檔時,定義base URI非常重要。
如果你不想使用abs:
前綴,還有一個方法能夠實現同樣的功能 .absUrl(String key)
。
–
問題描述:
在某些網站中經常會提供用戶評論的功能,但是有些不懷好意的用戶,會搞一些腳本到評論內容中,而這些腳本可能會破壞整個頁面的行為,更嚴重的是獲取一些機要信息,此時需要清理該HTML,以避免跨站腳本攻擊(XSS)。
解決方式:
使用clean()
方法清除惡意代碼,但需要指定一個配置的 Safelist
(舊版本中是Whitelist
),通常使用Safelist.basic()
即可。Safelist
的工作原理是將輸入的 HTML 內容單獨隔離解析,然后遍歷解析樹,只允許已知的安全標簽和屬性輸出。
String unsafe = "<p><a οnclick='attack()'>Link</a></p>"; // 輸出: <p><a >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
說明:
Safelist
不僅能夠在服務器端對用戶輸入的HTML進行過濾,只輸出一些安全的標簽和屬性,還可以限制用戶可以輸入的標簽范圍。
問題描述:
在某些網站中經常會提供用戶評論的功能,但是有些不懷好意的用戶,會搞一些腳本到評論內容中,而這些腳本可能會破壞整個頁面的行為,更嚴重的是獲取一些機要信息,此時需要清理該HTML,以避免跨站腳本攻擊(XSS)。
解決方式:
使用clean()
方法清除惡意代碼,但需要指定一個配置的 Safelist
(舊版本中是Whitelist
),通常使用Safelist.basic()
即可。Safelist
的工作原理是將輸入的 HTML 內容單獨隔離解析,然后遍歷解析樹,只允許已知的安全標簽和屬性輸出。
String unsafe = "<p><a οnclick='attack()'>Link</a></p>"; // 輸出: <p><a >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
說明:
jsoup的Safelist
不僅能夠在服務器端對用戶輸入的HTML進行過濾,只輸出一些安全的標簽和屬性,還可以限制用戶可以輸入的標簽范圍。
–
Connection.Response execute = Jsoup.connect("http://csdn.net/") .proxy("12.12.12.12", 1080) // 使用代理 .execute();
如果你讀完覺得有收獲,不妨點個贊~
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
方 案 | 實現方式 | 優點 | 缺點 |
---|---|---|---|
vm vh | 1.按照設計稿的尺寸,將px按比例計算轉為vw和vh | 1.可以動態計算圖表的寬高,字體等,靈活性較高 2.當屏幕比例跟 ui 稿不一致時,不會出現兩邊留白情況 | 1.每個圖表都需要單獨做字體、間距、位移的適配,比較麻煩 |
scale | 1.通過 scale 屬性,根據屏幕大小,對圖表進行整體的等比縮放 | 1.代碼量少,適配簡單2.一次處理后不需要在各個圖表中再去單獨適配 3.文字,圖片等大小均能自動適配 | 1.因為是根據 ui 稿等比縮放,當大屏跟 ui 稿的比例不一樣時,會出現周邊留白情況2.當縮放比例過大時候,字體會有一點點模糊,就一點點3.當縮放比例過大時候,事件熱區會偏移。 |
插件v-scale-screen | 是使用 css 屬性 transform 實現縮放效果的一個大屏自適應組件,通過 scale 進行等比例計算,達到等比例縮放的效果 | 可以通過api調整原稿的寬高 |
1.當屏幕正好為16:9的時候
2.當屏幕的尺寸比例大于 16:9 (左右拉長)
3.當屏幕的尺寸比例小于 16:9 時(左右變窄或者上下拉高)
實現方法:
css 方案 - sass
utils.scss
// 使用 scss 的 math 函數,https://sass-lang.com/documentation/breaking-changes/slash-div @use "sass:math"; // 默認設計稿的寬度 $designWidth: 1920; // 默認設計稿的高度 $designHeight: 1080; // px 轉為 vw 的函數 @function vw($px) { @return math.div($px, $designWidth) * 100vw; } // px 轉為 vh 的函數 @function vh($px) { @return math.div($px, $designHeight) * 100vh; } 復制代碼
![]()
路徑配置只需在vue.config.js里配置一下utils.scss的路徑,就可以全局使用了
vue.config.js
module.exports = { css: { loaderOptions: { sass: { prependData: `@import "@/assets/css/utils.scss";` } } }, } 在 .vue 中使用 <template> <div class="box"> </div> </template> <script> export default{ name: "Box", } </script> <style lang="scss" scoped="scoped"> /* 直接使用 vw 和 vh 函數,將像素值傳進去,得到的就是具體的 vw vh 單位 */ .box{ width: vw(300); height: vh(100); font-size: vh(16); background-color: black; margin-left: vw(10); margin-top: vh(10); border: vh(2) solid red; } </style>
![]()
css 方案 - less
utils.less @charset "utf-8"; // 默認設計稿的寬度 @designWidth: 1920; // 默認設計稿的高度 @designHeight: 1080; .px2vw(@name, @px) { @{name}: (@px / @designWidth) * 100vw; } .px2vh(@name, @px) { @{name}: (@px / @designHeight) * 100vh; } .px2font(@px) { font-size: (@px / @designWidth) * 100vw; } 路徑配置在vue.config.js里配置一下utils.less <style lang="less" scoped="scoped"> /* 直接使用 vw 和 vh 函數,將像素值傳進去,得到的就是具體的 vw vh單位 */ .box{ .px2vw(width, 300); .px2vh(height, 100); .px2font(16); .px2vw(margin-left, 300); .px2vh(margin-top, 100); background-color: black; } </style> 定義 js 樣式處理函數 // 定義設計稿的寬高 const designWidth = 1920; const designHeight = 1080; // px轉vw export const px2vw = (_px) => { return (_px * 100.0) / designWidth + 'vw'; }; export const px2vh = (_px) => { return (_px * 100.0) / designHeight + 'vh'; }; export const px2font = (_px) => { return (_px * 100.0) / designWidth + 'vw'; };
![]()
屏幕變化后,圖表自動調整
這種使用方式有個弊端,就是屏幕尺寸發生變化后,需要手動刷新一下才能完成自適應調整
為了解決這個問題,你需要在各個圖表中監聽頁面尺寸變化,重新調整圖表,在 vue 項目中,也可以借助element-resize-detector,最好封裝個 resize 的指令,在各圖表中就只要使用該指令就可以了,畢竟作為程序員,能偷懶就偷懶
解決方案一
npm install element-resize-detector --save
// directive.js import * as ECharts from "echarts"; import elementResizeDetectorMaker from "element-resize-detector"; import Vue from "vue"; const HANDLER = "_vue_resize_handler"; function bind(el, binding) { el[HANDLER] = binding.value ? binding.value : () => { let chart = ECharts.getInstanceByDom(el); if (!chart) { return; } chart.resize(); }; // 監聽綁定的div大小變化,更新 echarts 大小 elementResizeDetectorMaker().listenTo(el, el[HANDLER]); } function unbind(el) { // window.removeEventListener("resize", el[HANDLER]); elementResizeDetectorMaker().removeListener(el, el[HANDLER]); delete el[HANDLER]; } // 自定義指令:v-chart-resize 示例:v-chart-resize="fn" Vue.directive("chart-resize", { bind, unbind });
![]()
import '@/directive/directive';
<template> <div class="linechart"> <div ref="chart" v-chart-resize class="chart"></div> </div> </template>
這里要注意的是,圖表中如果需要 tab 切換動態更新圖表數據,在更新數據時一定不要用 echarts 的 dispose 方法先將圖表移除,再重新繪制,因為 resize 指令中掛載到的圖表實例還是舊的,就監聽不到新的 chart 元素的 resize 了,更新數據只需要用 chart 的 setOption 方法重新設置配置項即可。
解決方案二
1.在echarts中可以echarts.init(chatDom).resize()來解決寬高的自適應問題
let chatDom = document.getElementById('main'); let myChart = this.$echarts.init(chatDom); //根據父盒子的尺寸調整echarts的大小 setTimeout(() => { window.addEventListener('resize', () => { this.$echarts.init(chatDom).resize(); }); }, 20);
2.在DataV中可以添加key來解決
<dv-water-level-pond :config="config2" :key="key" ref="pie2" /> data(){ return { key: 1 } }, mounted() { this.pieOutlineFunc(); }, methods: { pieOutlineFunc() { var _this = this; window.addEventListener('resize', function (e) { _this.$nextTick(() => { console.log(_this.$refs.pie2); _this.key++; }); }); } }
![]()
通過 css 的 scale 屬性,根據屏幕大小,用js監測屏幕的大小變化對圖表進行整體的等比縮放,從而達到自適應效果
當屏幕的尺寸比例剛好是 16:9 時,頁面能剛好全屏展示,內容占滿顯示器
當屏幕尺寸比例大于 16:9 時,上下左右留白,左右占滿并居中,顯示比例保持 16:9
當屏幕尺寸比例大于 16:9 時,頁面左右留白,上下占滿并居中,顯示比例保持 16:9
上代碼
html
<template> <div class="screen-root"> <div class="screen" id="screen"> <div class="div1"> <h1>11111111111</h1> </div> <div class="div2"> <h1>2222222222</h1> </div> <div class="div3"> <h1>3333333333</h1> </div> </div> </div> </template>
js
<script> export default { mounted() { // 初始化自適應 ----在剛顯示的時候就開始適配一次 this.handleScreenAuto(); // 綁定自適應函數 ---防止瀏覽器欄變化后不再適配 window.onresize = () => this.handleScreenAuto(); }, deleted() { window.onresize = null; }, methods: { // 數據大屏自適應函數 handleScreenAuto() { const designDraftWidth = 1920; //設計稿的寬度 const designDraftHeight = 1080; //設計稿的高度 // 根據屏幕的變化適配的比例 const scale = document.documentElement.clientWidth / document.documentElement.clientHeight < designDraftWidth / designDraftHeight ? document.documentElement.clientWidth / designDraftWidth : document.documentElement.clientHeight / designDraftHeight; // 縮放比例 document.querySelector('#screen').style.transform = `scale(${scale}) translate(-50%,-50%)`; return 1; } } }; </script>
![]()
css
<style lang="scss" scoped> /* 除了設計稿的寬高是根據您自己的設計稿決定以外,其他復制粘貼就完事 */ h1 { color: red; font-size: 50px; } .screen-root { height: 100vh; width: 100vw; .screen { display: inline-block; width: 1920px; //設計稿的寬度 height: 1080px; //設計稿的高度 transform-origin: 0 0; position: absolute; left: 50%; top: 50%; border: 2px solid rgb(31, 210, 145); box-sizing: border-box; display: flex; .div1 { background-color: #fff; height: 100%; text-align: center; flex: 0 1 30%; } .div2 { background-color: rgb(133, 14, 14); height: 100%; text-align: center; flex: 0 1 40%; } .div3 { background-color: rgb(61, 6, 188); height: 100%; text-align: center; flex: 0 1 30%; } } } </style>
![]()
它其實也是通過 scale 進行等比例計算放大和縮小的,和方案二的原理是一樣的,還可以通過api調整樣式,源碼地址和對應的API
使用方法:
1.vue2請使用v-scale-screen@1.0.0版本,vue3請使用v-scale-screen@2.0.0版本
npm install v-scale-screen@1.0.0 -save
# or
yarn add v-scale-screen
2.使用-vue2中使用插件導入,vue3以組件導入
vue2 // main.js import VScaleScreen from 'v-scale-screen' Vue.use(VScaleScreen) 組件內使用 //html <v-scale-screen width="1920" height="1080" :boxStyle="boxStyle"> <div> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> </div> </v-scale-screen> //js data() { return { boxStyle: { backgroundColor: 'green' }, }
![]()
vue3 <v-scale-screen width="1920" height="1080"> <div> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> <v-chart>....</v-chart> </div> </v-scale-screen> <script> import VScaleScreen from 'v-scale-screen' export default { components:{ VScaleScreen } } </script>
![]()
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
在本教程中,我將向大家展示如何使用 Laravel + Vue 3 使用 typescript 和 Vite 設置你自己的單頁應用程序。
這是在 Laravel 項目中添加 PWA 的手動方法。我們不會使用 InertiaJS 或其他類似的東西,我們也不會混合使用。我們將手動實現我們自己的 VueJS 前端。
composer create-project laravel/laravel laravel-vue-manual
在我們的 laravel 項目中,讓我們使用 yarn 運行一個命令,并選擇 vue 和 typescript。
yarn create vite
將項目名稱設置為:FrontEndApp
選擇:Vue
選擇:TypeScript
然后轉到我們的FrontEndApp目錄并運行yarn或yarn install安裝依賴項。
配置 Vite
讓我們配置我們的 vite 配置FrontEndApp\vite.config.ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; export default ({ mode }) => { // 檢查是否開發 const isDevelopment = mode === "development"; return defineConfig({ server: { port: 3000, }, build: { // 生成的文件將添加到此處 outDir: "./../public/app", }, // 也將更改基于模式的基礎 base: isDevelopment ? "/" : "/app/", plugins: [vue()], }); };
![]()
然后讓我們更改build腳本FrontEndApp\package.json,這樣每次我們構建它時都會替換以下文件public/app:
{ ... "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build --emptyOutDir", "preview": "vite preview" }, ... }
現在,如果我們在FrontEndApp中運行yarn build
,它應該在laravel項目的根目錄中的public
文件夾中創建一個名為 app 的文件夾。
讓我們設置我們的 laravel 路由,以便我們可以訪問我們剛剛創建的文件。
讓我們編輯這個文件 routes\web.php
<?php
use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); Route::get('/app/{any}', function () { $path = public_path('app/index.html'); abort_unless(file_exists($path), 400, 'Page is not Found!'); return file_get_contents($path); }) ->name('FrontEndApp');
現在,如果我們http://127.0.0.1:8000/app
在瀏覽器中打開,我們現在可以看到我們的應用程序已啟動。
我們將在我們的根項目目錄中添加一個開發包,并同時調用它。我們用它來一次運行 2 個或更多命令。
安裝:
yarn add -D concurrently
如果我們想要自動工作,不想每次使用時都重新構建frontednapp,我們要做的是在package.json項目的根目錄中添加一個新腳本。
{ ... "scripts": { ... "front:serve": "cd FrontEndApp && yarn dev", "front:build": "cd FrontEndApp && yarn build", "serve": "concurrently \"php artisan serve --port=8080\" \"yarn front:serve\"", "deploy": "yarn setup && yarn front:build && php artisan migrate" }, ... }
這樣, running yarn serve
將同時運行 127.0.0.1:8080
和 localhost:3000
。你現在可以同時使用這兩個項目。
完成 FrontEndApp 的工作后,你可以運行 yarn deploy
以構建我們的前端。
我相信這也是大家可以在 laravel 項目中添加 pwa 的一種方式,這樣你就可以將它們保存在一個項目中。
考慮到這一點,你可以添加routes到你的 FrontEndApp 項目中,還可以添加狀態管理器PiniaJA,例如 等等。
文章來源:csdn
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務、UI設計公司、界面設計公司、UI設計服務公司、數據可視化設計公司、UI交互設計公司、高端網站設計公司、UI咨詢、用戶體驗公司、軟件界面設計公司
藍藍設計的小編 http://www.syprn.cn