環境資訊中心綜合外電;姜唯 編譯;彭瑞祥 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!
摘錄自2019年9月23日公視報導
瑞士居民為阿爾卑斯山的冰川舉行了葬禮,受氣候變遷影響,這座冰川從2006年消融速度加快,現在已經消失了90%面積。
大約250個瑞士居民,22日穿著黑衣,披著黑頭紗爬了約兩小時的路程,登上海拔約2700公尺的皮措爾山山頂,為這座即將消失的冰川舉行葬禮。
瑞士蘇黎世聯邦理工學院冰川專家赫斯表示,「照目前情況來看,我們還有約4個足球場大小的冰川,但過去兩年冰川消融的速度迅速增加。」
皮措爾冰川位在瑞士境內的阿爾卑斯山,自從2006年以來,已經失去了將近90%面積,現在只剩下約兩萬6000平方公尺,不到四個足球場大小,科學家認為,冰川消融如此快速是受到氣候變遷影響,如果再不控制溫室氣體排放,這座冰川將會在2030年前完全消失。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※FB行銷專家,教你從零開始的技巧
使用Spring Bboot是快樂並且簡單的,不需要繁瑣的配置就能夠完成一套非常強大的應用。
Spring Boot 2.3.1 發佈於:2020/06/12,現在已經提交到 Spring 倉庫和 Maven 中央倉庫了。
這個版本包括 127 個 bug 修復、Spring Boot 文檔改進增強、依賴升級等,另外還新增了一些新特性:
•提供基於新的 Maven 坐標 com.oracle.database 對 Oracle JDBC driver 的依賴管理;
•優化 Spring Cloud 的 CachedRandomPropertySource 不能正確適配的問題;•限制使用定製的 YAML 類型;
•增強對 NoSuchMethodErrors 異常失敗分析,能显示基類從哪被加載的;•提供更佳的錯誤消息,如果 Docker 停止運行了;
•優化 SystemEnvironmentPropertyMapper 類;
•提供更佳的診斷信息,當構建 OCI 鏡像失敗時 Docker 響應的 500 錯誤;
•支持通過 alwaysUseFullPath=true 參數來配置 UrlPathHelper;•支持在 Elasticsearch URIs 中使用用戶信息;
•支持在 Spring WebFlux 框架中使用歡迎頁面;
這個小版本還增加了蠻多東西的,大家也沒有必要跟着版本走,可以根據需要進行升級。疫情也擋不住外國友人更新的熱情。
使用STS,可以去官方網站下載最新版。網站地址 https://Spring.io/tools/sts/ Spring Tool Suite™是基於eclipse開發的專門為Spring開發使用的工具包。
選擇Spring Starter Project,
輸入工程名 對應的Name 打包方式 對應的Packaging,可以選擇jar或者war的方式。
輸入組織名 對應的Group 輸入描述 對應的Description
輸入包名 對應的Package 點擊next,然後選擇web和mysql
這裏的版本用的是2.3.1 如果沒有本地maven庫或者私庫會下載很長時間。
進入 Chapter0301Application 添加
@RestController @SpringBootApplication public class Chapter0301Application { @RequestMapping("/") String home() { return "歡迎使用Spring Boot!"; } public static void main(String[] args) { SpringApplication.run(Chapter0301Application.class, args); } }
使用@RestController 相當於@Controller 和 @RequestBody。是Sspring Bboot 基於Sspring MVC的基礎上進行了改進, 將@Controller 與@ResponseBody 進行了合併形成的一個新的註解。 @EnableAutoConfiguration 作用 從classpath中搜索所有META-INF/spring.factories配置文件然後,將其中org.springframework.boot.autoconfigure.EnableAutoConfiguration key對應的配置項加載到spring容器 只有spring.boot.enableautoconfiguration為true(默認為true)的時候,才啟用自動配置 @EnableAutoConfiguration還可以進行排除,排除方式有2種,一是根據class來排除(exclude),二是根據class name(excludeName)來排除 其內部實現的關鍵點有
1.ImportSelector 該接口的方法的返回值都會被納入到spring容器管理中
2.SpringFactoriesLoader 該類可以從classpath中搜索所有META-INF/spring.factories配置文件,並讀取配置
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE) 2020-06-23 13:30:11.611 INFO 9916 --- [ main] com.cloud.sky.Chapter0301Application : Starting Chapter0301Application on DADI-PC with PID 9916 (D:\java\microservice\chapter0301\target\classes started by Administrator in D:\java\microservice\chapter0301) 2020-06-23 13:30:11.614 INFO 9916 --- [ main] com.cloud.sky.Chapter0301Application : No active profile set, falling back to default profiles: default 2020-06-23 13:30:12.415 INFO 9916 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2020-06-23 13:30:12.423 INFO 9916 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-06-23 13:30:12.424 INFO 9916 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36] 2020-06-23 13:30:12.512 INFO 9916 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-06-23 13:30:12.512 INFO 9916 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 830 ms 2020-06-23 13:30:12.665 INFO 9916 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-06-23 13:30:12.809 INFO 9916 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2020-06-23 13:30:12.818 INFO 9916 --- [ main] com.cloud.sky.Chapter0301Application : Started Chapter0301Application in 1.492 seconds (JVM running for 3.109) 2020-06-23 13:30:20.675 INFO 9916 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-06-23 13:30:20.676 INFO 9916 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-06-23 13:30:20.680 INFO 9916 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
打開瀏覽器訪問 http://localhost:8080/ 可以得到如下頁面
構建的過程中遇到問題
[INFO] Scanning for projects... [ERROR] [ERROR] Some problems were encountered while processing the POMs: [FATAL] Non-parseable POM D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom: entity reference names can not start with character ')' (position: START_TAG seen ...ost,s="";function qs(n){var u=D.URL;var t=u.match(eval(\'/(\\?|#|&)... @1:243) @ D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom, line 1, column 243 @ [ERROR] The build could not read 1 project -> [Help 1] [ERROR] [ERROR] The project com.cloudskyme:chapter0301:0.0.1 (D:\java\microservice\chapter0301\pom.xml) has 1 error [ERROR] Non-parseable POM D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom: entity reference names can not start with character ')' (position: START_TAG seen ...ost,s="";function qs(n){var u=D.URL;var t=u.match(eval(\'/(\\?|#|&)... @1:243) @ D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom, line 1, column 243 -> [Help 2] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException [ERROR] [Help 2] http://cwiki.apache.org/confluence/display/MAVEN/ModelParseException
修改maven默認源配置
我使用的是阿里的maven倉庫,國外的東西沒個代理還真麻煩。
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>sonatype-nexus-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
然後執行 mvn help:system
成功可以看到如下界面:
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
目錄
我們用golang做odbc驅動開發的任務並不多,隔段時間可能會來一個。每次開發會忘記如何配置數據源和對應的數據源鏈接參數配置。這裏做一個整理。
ODBC(Open Database Connectivity)是一組對數據庫訪問的標準API,其最大的優點是以統一的方式處理所有的數據庫。
JDBC(Java Database Connectivity)是Java與數據庫的接口規範,允許Java程序發送SQL指令並處理結果。比較常見JdbcTemplate
打開控制面板找到管理工具,當前目錄有兩個ODBC的配置工具。分別是:ODBC Data Sources (32-bit)、ODBC 數據源(64 位)。顧名思義一個是32位,一個是64位。在配置ACCESS數據源時需要選擇32位。
我們也可以直接在系統目錄下找到對應的可執行文件。
1)32位:%windir%\syswow64\odbcad32.exe
2)64位:%windir%\system32\odbcad32.exe
題外話:syswow64 目錄存放的是32位的程序,system32目錄存放的是64位的程序,在註冊dll的時候需要注意下。ITDragon 在接觸驅動開發之前一直都弄反了。這篇文章做了通俗易懂地解釋https://www.cnblogs.com/hbccdf/p/dllchecktoolandsyswow64.html
網上收集整理,不保證正確性,僅供參考。
| 數據庫 | 連接參數 |
|---|---|
| MySQL | driver={mysql};database=數據庫;uid=賬號;pwd=密碼; |
| MSSQL Server | driver={sql server};server=服務器;database=數據庫;uid=ITDragon;pwd=密碼; |
| Access | driver={microsoft access driver (*.mdb)};dbq=mdb文件全路徑;uid=ITDragon;pwd=密碼; |
| SQLite | driver={SQLite3 ODBC Driver};database=db文件全路徑 |
| PostgreSQL | driver={PostgreSQL ANSI};server=服務器;uid=賬號;pwd=密碼;database=數據庫; |
| DBase | driver={microsoft dbase driver (*.dbf)};driverid=277;dbq=dbf文件全路徑; |
| Oracle | driver={microsoft odbc for oracle};server=服務器;uid=ITDragon;pwd=密碼; |
| MS text | driver={microsoft text driver (* .txt; *.csv)};dbq=文件全路徑;extensions=asc,csv,tab,txt;PersistSecurityInfo=false; |
| Visual Foxpro | driver={microsoft Visual Foxpro driver};sourcetype=DBC;sourceDB=*.dbc;Exclusive=No; |
第一步:雙擊ODBC 數據源(64 位),可以選擇用戶DSN(系統只對當前用戶生效),也可以選擇系統DSN(能登錄當前系統的用戶都生效)
第二步:點擊添加,選擇事先安裝好的MySQL ODBC xx Driver 驅動後點擊完成。注意不同版本之間對某些sql語法的支持略有不同(之前吃過這個虧,我的環境有問題,客戶環境沒問題)。
第三步:完善基本鏈接信息後點擊Test,提示鏈接成功後點擊OK完成配置。
因為ODBC驅動配置已經將數據庫的連接地址、賬號、密碼、數據庫都已經配置完成,連接參數只需要指定驅動名稱即可:DSN=ITDragon_MySQL
Golang偽代碼:
import (
"database/sql"
_ "github.com/alexbrainman/odbc"
)
db, err := sql.Open("odbc", "DSN=ITDragon_MySQL")
if err != nil {
panic(err)
}
defer db.Close()
第一步:雙擊ODBC 數據源(64 位),可以選擇用戶DSN(系統只對當前用戶生效),也可以選擇系統DSN(能登錄當前系統的用戶都生效)
第二步:點擊添加,選擇SQL Server。這一項我ITDragon 並沒有手動安裝,應該是安裝SQL Server數據庫的時候自動安裝上去的。
第三步:鍵盤輸入需要連接的SQLServer服務器,如果是本機,就輸入計算機名。如果是遠程主機就需要輸入IP,Port 。不要點擊下拉框,會卡死。
第四步:選擇SQL Server驗證方式
第五步:選擇默認數據庫,也可以在寫sql語句時將表名的全路徑寫全(我ITDragon 習慣用寫全)
第六步:可以考慮修改SQL Server的系統消息語言,數據的加密,執行字符數據翻譯,修改日誌保存路徑等。也可以默認。
第七步:點擊完成,彈出“按照以下配置創建新的ODBC數據源”,點擊測試數據源,提示測試成功。點擊確定完成創建。
Golang偽代碼:
import (
"database/sql"
_ "github.com/alexbrainman/odbc"
)
db, err := sql.Open("odbc", "driver={sql server};server=DESKTOP-HKC2IA3;DSN=ODBCName;uid=xxx;pwd=xxx;")
if err != nil {
panic(err)
}
defer db.Close()
第一步:雙擊ODBC Data Sources (32-bit),而不是64位。可以選擇用戶DSN(系統只對當前用戶生效),也可以選擇系統DSN(能登錄當前系統的用戶都生效)
第二步:選擇Microsoft Access Driver (*.mdb)。點擊數據庫下發的選擇按照,選擇mdb文件,點擊確定完成配置。
我們其實可以不用配置Access的數據源,直接用DBQ指定mdb的文件路徑,再用pwd輸入密碼訪問。
Golang偽代碼:
import (
"database/sql"
_ "github.com/alexbrainman/odbc"
)
db, err := sql.Open("odbc", "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=全路徑.MDB;pwd=xx;")
if err != nil {
panic(err)
}
defer db.Close()
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※新北清潔公司,居家、辦公、裝潢細清專業服務
※推薦評價好的iphone維修中心
摘要:面對如何在現有的低版本的框架服務上,運行新版本的前端服務問題,華為雲前端推出了一種融合方案,該方案能讓獨立的Angular項目整體運行在低版本的框架服務上,通過各種適配手段,讓Angular項目也能獲取到外層框架服務的資源。
華為雲前端服務前期採用AngularJs作為框架技術棧,技術較為老舊,性能較差,在華為雲快速發展的今天,顯然不能滿足要求。因此我們必須要升級前端技術棧,使用Angular2+來承載我們的前端服務。GeminiDB作為新服務,也是數據庫乃至華為雲未來的重點服務,作為前端部分,必須在技術上使用最前沿的框架,以最大地提高用戶體驗。
但是技術棧的升級不是一蹴而就的,尤其是在華為雲,所有的雲服務必須在框架服務的底座上運行,而框架服務承載了所有的雲服務,如果要進行技術棧升級,必然是一個緩慢的過程。GeminiDB作為華為雲服務里的一員,也不可能脫離框架服務而存在。因此存在一個問題,就是如何在現有的低版本的框架服務上,運行新版本的前端服務。
為了解決以上問題,華為雲前端推出了一種融合方案,該方案能讓獨立的Angular項目整體運行在低版本的框架服務上,通過各種適配手段,讓Angular項目也能獲取到外層框架服務的資源。
底層項目使用webpack打包,打包后通過在index.html里引入businessAll.js文件,以該文件為入口啟動整個框架服務。
<script type="text/javascript" src="businessAll.js"></script>
在底層框架服務啟動后,再渲染出具體雲服務內容。
<div class="service-content-view" ui-view ng-animate="{enter:'fade-enter'}"></div>
Angular項目支持獨立運行,有單獨的index.html,也有單獨的main.ts入口。但是如果希望Angular項目運行在底層框架服務上,就必須把Angular項目看作是一個獨立的模塊,把項目整體引入到底層項目中。因此,我們可以預先把Angular項目編譯好,放到底層項目的一個目錄下。在運行底層項目時,在index.html里將Angular項目引進來,獨立運行。
<link rel="stylesheet" type="text/css" href="{底層項目中Angular項目的路徑}/styles.css" /> <script type="text/javascript" src="{底層項目中Angular項目的路徑}/runtime.js"></script> <script type="text/javascript" src="{底層項目中Angular項目的路徑}/polyfills.js"></script> <script type="text/javascript" src="{底層項目中Angular項目的路徑}/main.js"></script>
底層項目和Angular項目均能獨立,但是要讓兩者融合起來,會遇到以下幾個問題:
1.底層項目中如何渲染出Angular項目。
2.Angular項目依賴底層項目的資源,如何保證Angular項目在底層項目運行起來后再運行。
3.如何解決底層項目和Angular項目的路由衝突問題。
底層項目分為兩部分,一部分是底層框架服務,另一部分是具體雲服務。現在我們要做的是把老的雲服務項目替換成新的Angular項目,因此我們可以直接在渲染老的雲服務的地方替換成新的Angular項目的渲染容器。
<div class="service-content-view" ui-view ng-animate="{enter:'fade-enter'}"></div> <app-root></app-root>
底層框架服務對頁面渲染上做了一些體驗上的優化,因此必須保留原模板中的ui-view,使底層項目正常運行起來,實際上老的雲服務項目的渲染內容已經轉發到新的Angular項目上面。
底層框架服務給雲服務提供了很多公共變量與服務,這些變量和服務是各個雲服務必須要使用的,否則雲服務將不能正常運作。
對於Angular項目來說,要使用底層框架服務提供的內容,首先要求Angular項目在底層項目運行起來之後再運行。這裏採用Augular中的APP_INITIALIZER令牌來解決這個問題。APP_INITIALIZER是一個函數,在程序初始化的時候被調用。這裡在根模塊的providers中以factory的形式來配置。
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppInitService } from './services/app-init.service';
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [
AppInitService,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [AppInitService],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
export function initializeApp(appInitService: AppInitService) {
return (): Promise<any> => {
return appInitService.Init();
};
}
在appInitService里,先獲取到底層框架的資源,再進行Angular項目的初始化。
import { Injectable } from '@angular/core';
@Injectable()
export class AppInitService {
constructor() {}
Init() {
return new Promise<void>((resolve, reject) => {
// 獲取到底層框架服務的資源
resolve();
});
}
}
底層項目使用的是AngularJs,Angular項目獲取底層框架服務提供的資源不能通過Angular的方式引入,因此需要藉助AngularJS的注入器獲取在底層框架中註冊的服務組件:
static get(inject: string): any { return (window as any).angular.element('html').injector().get(inject);} 如,要獲取 $rootScope: rootScope = (window as any).angular.element('html').injector().get(‘$rootScope’);
Angular項目本身有自己的路由,但是Angular項目是運行在底層框架之上的,Angular項目的路由將會被底層框架所攔截。因此,我們也需要在底層框架的項目中配置相同的路由,以免Angular項目中的有效路由被底層框架識導向為404。
Angular項目路由:
{ path: '', redirectTo: 'ng2app1', pathMatch: 'full' }, { path: 'ng2app1', loadChildren: './ng2app1/ng2app1.module#Ng2app1Module', }, { path: 'ng2app2', loadChildren: './ng2app2/ng2app2.module#Ng2app2Module', } 底層框架路由: var configArr = [ { name: 'ng2app1', url: '/ng2app1' }, { name: 'ng2app2', url: '/ng2app2' } ];
另外,由於底層項目使用的是hash路由,Angular項目中也要做相應的配置,默認是使用的是PathLocationStrategy,需要切換到hash模式。
import { LocationStrategy, HashLocationStrategy } from '@angular/common';
...
providers: [
{
provide: LocationStrategy,
useClass: HashLocationStrategy
}
]
以上方案是在底層框架升級周期長的前提下的一個臨時方案,實際上還是存在着不少的問題。比如底層框架對於老的雲服務容器是有統一管理的,老的雲服務容器會針對不同的場景能夠自適應,而融合方案中的Angular項目則不能;每次啟動整個項目時,必須要預先編譯好裏面的Angular項目,再去啟動外層的底層框架,開發效率比較低。因此,後續GeminiDB服務應該在底層框架升級后,儘快適應到新的底層框架體系中,提高服務的可用性和穩定性。
點擊關注,第一時間了解華為雲新鮮技術~
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能
※台北網頁設計公司這麼多該如何選擇?
※智慧手機時代的來臨,RWD網頁設計為架站首選
※評比南投搬家公司費用收費行情懶人包大公開
※幫你省時又省力,新北清潔一流服務好口碑
※回頭車貨運收費標準
如果你想從頭學習Jmeter,可以看看這個系列的文章哦
https://www.cnblogs.com/poloyy/category/1746599.html
我們通過實際栗子去講述理論知識點
| 字段 | 含義 |
| Apply to | 應用範圍,選默認的 main sample only 就行了 |
| Field to check |
可提取的字段 |
| Names of created variables |
|
| Regular Expression |
正則表達式 |
| Template | 從找到的匹配項中創建字符串的模板 |
| Match No.(0 for Random) |
|
| Default Value |
|
| Use empty default value |
勾選后,提取不到值時,則返回空字符串 |
| 屬性 | 含義 |
| Body | 響應體,不包括響應頭;最常用 |
| Body (unescaped) | 響應體,替換了所有HTML轉義符;不建議使用 |
| Body as a Document | 從不同類型的文件中提取文本;影響性能 |
| Request Headers | 請求頭 |
| Response Headers | 響應頭 |
| URL | URL |
| Response Code | 響應碼 |
| Response Message | 響應信息 |
這個栗子,我都會以這個地址的接口來完成 JSON 提取器的實戰慄子,大家可以註冊個賬號玩一玩哦
http://api.yesapi.cn/docs.php?keyword=%E4%BC%9A%E5%91%98&channel=api
下面多個栗子都以這個測試計劃為基礎哦
登錄是執行其他接口的前置接口,所以要獲取用戶登錄后的 token、uuid
下面的栗子都以這個 JSON 字符串為基礎,从里面提取結果
這 JSON 字符串也是某個接口的響應內容,貨真價實,感興趣也可以自己玩一玩:http://api.yesapi.cn/docs-api-App.User.GetList.html
{ "ret": 200, "msg": "V2.5.1 YesApi App.User.GetList", "data": { "total": 4, "err_msg": "", "err_code": 0, "users": [ { "role": "user", "status_desc": "正常", "reg_time": "2020-06-22 20:45:05", "role_desc": "普通會員", "ext_info": { "yesapi_nickname": "", "yesapi_points": 0 }, "uuid": "0564CE592B4CE914365D8922F6FC4CEC", "username": "luojunjiess286", "status": 0 }, { "role": "user", "status_desc": "正常", "reg_time": "2020-06-22 14:27:17", "role_desc": "普通會員", "ext_info": { "yesapi_nickname": "", "yesapi_points": 0 }, "uuid": "0164DC0680F84DCE40D3DD4A36640ECA", "username": "luojunjiessa", "status": 0 }, { "role": "admin", "status_desc": "正常", "reg_time": "2020-03-23 22:48:32", "role_desc": "管理員", "ext_info": { "yesapi_nickname": "", "yesapi_points": 0 "yesapi_reg_source": "" }, "uuid": "079BF6BB82AFCFC7084F96AECAF0519F", "username": "luojunjiess", "status": 0 } ] } }
就是正則表達式里只有一個 ( ) ,且 Match No. 不是 -1
uuid1= uuid1_g=1 uuid1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid1_g1=0564CE592B4CE914365D8922F6FC4CEC
如果正則匹配到值,但是沒有填模板,則返回空
uuid2="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid2_g=1 uuid2_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid2_g1=0564CE592B4CE914365D8922F6FC4CEC
uuid3=0564CE592B4CE914365D8922F6FC4CEC uuid3_g=1 uuid3_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid3_g1=0564CE592B4CE914365D8922F6FC4CEC
uuid4=null uuid4_g=1 uuid4_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid4_g1=0564CE592B4CE914365D8922F6FC4CEC
$2$ 模板並不存在,其實就是 uuid4_g2 變量不存在,即使勾了使用空默認值,也返回 null,
有兩種情況
手動分成四部分
uuid1_1=0564CE592B4CE914365D8922F6FC4CEC uuid1_1_g=1 uuid1_1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC" uuid1_1_g1=0564CE592B4CE914365D8922F6FC4CEC uuid1_2=0164DC0680F84DCE40D3DD4A36640ECA uuid1_2_g=1 uuid1_2_g0="uuid":"0164DC0680F84DCE40D3DD4A36640ECA" uuid1_2_g1=0164DC0680F84DCE40D3DD4A36640ECA uuid1_3=079BF6BB82AFCFC7084F96AECAF0519F uuid1_3_g=1 uuid1_3_g0="uuid":"079BF6BB82AFCFC7084F96AECAF0519F" uuid1_3_g1=079BF6BB82AFCFC7084F96AECAF0519F uuid1_matchNr=3
info1= info1_g=2 info1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286" info1_g1=0564CE592B4CE914365D8922F6FC4CEC info1_g2=luojunjiess286
info2=0564CE592B4CE914365D8922F6FC4CEC info2_g=2 info2_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286" info2_g1=0564CE592B4CE914365D8922F6FC4CEC info2_g2=luojunjiess286
info3=0564CE592B4CE914365D8922F6FC4CECluojunjiess286 info3_g=2 info3_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286" info3_g1=0564CE592B4CE914365D8922F6FC4CEC info3_g2=luojunjiess286 info4=0564CE592B4CE914365D8922F6FC4CEC,luojunjiess286 info4_g=2 info4_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286" info4_g1=0564CE592B4CE914365D8922F6FC4CEC info4_g2=luojunjiess286
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
chromedp是go寫的,支持Chrome DevTools Protocol 的一個驅動瀏覽器的庫。並且它不需要依賴其他的外界服務(比如 Selenium 和 PhantomJs)。
Chrome DevTools Protocol (CDP) 的主頁在:https://chromedevtools.github.io/devtools-protocol/。 它提供一系列的接口來查看,檢查,調整並且檢查 Chromium 的性能。Chrome 的開發者工具就是使用這一系列的接口,並且 Chrome 開發者工具來維護這些接口。
所謂 CDP 的協議,本質上是什麼呢?本質上是基於 websocket 的一種協議。比如
在我們打開 webtool 調試工具的時候,其實調試工具也是一個web頁面,兩個web頁面通過websocket建立了一個聯繫。
所以我們如果寫了一個客戶端程序,也和目標頁面創建一個基於 CDP 的 websocket連接,我們也可以通過這個協議來對頁面進行操作。
在chrome的開發者工具中
打開實驗選項 Protocol Monitor
重啟chrome,在console的更多裏面就可以打開對應的 Monitor
我們從 Protocol Monitor 面板中可以看到,其中有幾個字樣,Method,Request,Response。
這裏的 Method 就是對應官網 https://chromedevtools.github.io/devtools-protocol/ 左側每個Domain的 Event。
這裏的每個Method方法可能是調試頁面給目標頁面發送的,但是更多是目標頁面給調試頁面發送的消息。所以我們需要讀懂每個Method的內容。不過很可惜,我個人感覺官網的每個Method文檔的描述寫的實在是太簡單了,也沒有看到更詳細的描述,只能通過名字和事件來猜測每個Method意思了。
chromedp的使用最快的方法就是看 https://github.com/chromedp/examples 這個項目
基本我們可以熟悉最常用的幾個方法了:
我們嘗試打開 https://www.cnblogs.com/ 的首頁,然後獲取所有文章的標題和鏈接:
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()
var nodes []*cdp.Node
err := chromedp.Run(ctx,
chromedp.Navigate("https://www.cnblogs.com/"),
chromedp.WaitVisible(`#footer`, chromedp.ByID),
chromedp.Nodes(`.//a[@class="titlelnk"]`, &nodes),
)
if err != nil {
log.Fatal(err)
}
fmt.Println("get nodes:", len(nodes))
// print titles
for _, node := range nodes {
fmt.Println(node.Children[0].NodeValue, ":", node.AttributeValue("href"))
}
}
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
作者:凹凸曼 – 暖暖
SVG 即 Scalable Vector Graphics 可縮放矢量圖形,使用XML格式定義圖形。
SVG 的應用十分廣泛,得益於 SVG 強大的各種特性。
可利用 SVG 矢量的特點,描出深圳地鐵的輪廓:
SVG 可依據一定的規則,轉成 iconfont 使用:
利用 SVG 的 foreignObject 標籤實現截圖功能,原理:foreignObject 內部嵌入 HTML 元素:
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="120" height="60">
<p style="font-size:20px;margin:0;">凹凸實驗室 歡迎您</p>
</foreignObject>
</svg>
截圖實現流程:
<foreignObject></foreignObject> 這對標籤;foreignObject 即可;Blob 構建 svg 對象;URL.createObjectURL(svg) 取出 URL。由於微信編輯器不允許嵌入 <style><script><a> 標籤,利用SVG SMIL 可進行微信公眾號極具創意的圖文排版設計,包括動畫與交互。
但是也要注意,標籤里不允許有id,否則會被過濾或替換掉。
點擊 “凹凸實驗室” 后,圍繞 “凹凸實驗室” 中心旋轉 360度,點擊0.5秒后 出現 https://aotu.io/ ,動畫只運行一次。
下圖為 GIF循環演示:
代碼如下:
<svg width="360" height="300" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- 點擊后 運行transform旋轉動畫,restart="never"表示只運行一次 -->
<animateTransform attributeName="transform" type="rotate" begin="click" dur="0.5s" from="0 100 80" to="360 100 80" fill="freeze" restart="never" />
<g>
<text font-family="microsoft yahei" font-size="20" x="50" y="80">
凹凸實驗室
</text>
</g>
<g style="opacity: 0;">
<!-- 同一個初始位置以及大致的寬高,觸發點擊事件 -->
<text font-family="microsoft yahei" font-size="20" x="50" y="80">https://aotu.io/</text>
<!-- 點擊后 運行transform移動動畫,改變文本的位置 -->
<animateTransform attributeName="transform" type="translate" begin="click" dur="0.1s" to="0 40" fill="freeze" restart="never" />
<!-- 點擊0.5秒后 運行opacity显示動畫 -->
<animate attributeName="opacity" begin="click+0.5s" from="0" to="1" dur="0.5s" fill="freeze" restart="never" />
</g>
</g>
</svg>
以上是鄙人對SVG的大致印象,最近的需求開發再次刷新了我的認知,那就是 SVG實現非比例縮放 以及 小程序不支持SVG標籤的處理,下面容我來講述一番。
我們熟知的 iconfont,可通過改變字體大小縮放,但是這是 比例縮放,那如何實現 SVG 的非比例縮放呢?
如下圖所示,如何將 一隻兔子 非比例縮放?
划重點:實現非比例縮放主要涉及三個知識點:viewport、viewBox和preserveAspectRatio,viewport 與viewBox 結合可實現縮放的功能,viewBox 與 preserveAspectRatio 結合可實現非比例的功能。
viewport 表示SVG可見區域的大小。
viewport 就像是我們的显示器屏幕大小,超出區域則隱藏,原點位於左上角,x 軸水平向右,y 軸垂直向下。
通過類似CSS的屬性 width、height 指定視圖大小:
<svg width="400" height="200"></svg>
viewBox值有4個数字:x, y, width, height 。
其中 x:左上角橫坐標,y:左上角縱坐標,width:寬度,height:高度。
原點默認位於左上角,x 軸水平向右,y 軸垂直向下。
<svg width="400" height="200" viewBox="0 0 200 100"></svg>
显示器屏幕的畫面,可以特寫,可以全景,這就是 viewBox。
viewBox 可以想象成截屏工具選中的那個框框,和 viewport 作用的結果就是 把框框中的截屏內容再次在 显示器 中全屏显示。
(圖片來源:SVG 研究之路 (23) – 理解 viewport 與 viewbox)
上圖的紅色框框和藍色框框,恰好和显示器的比例相同,如果是下圖的綠色框框,怎樣在显示器屏幕中显示呢?
preserveAspectRatio 作用的對象是 viewBox,使用方法如下:
preserveAspectRatio="[defer] <align> [<meetOrSlice>]"
// 例如 preserveAspectRatio="xMidYMid meet"
其中 defer 此時不是重點,暫且忽略,主要了解 align 和 meetOrSlice 的 用法:
align:由兩個名詞組成,分別代表 viewbox 與 viewport 的 x 方向、y方向的對齊方式。| 值 | 含義 |
|---|---|
xMin |
viewport 和 viewBox 左邊對齊 |
xMid |
viewport 和 viewBox x軸中心對齊 |
xMax |
viewport 和 viewBox 右邊對齊 |
YMin |
viewport 和 viewBox 上邊緣對齊。注意Y是大寫。 |
YMid |
viewport 和 viewBox y軸中心點對齊。注意Y是大寫。 |
YMax |
viewport 和 viewBox 下邊緣對齊。注意Y是大寫。 |
meetOrSlice:表示如何維持高寬的比例,有三個值 meet、slice、none。
meet – 默認值,保持縱橫比縮放 viewBox 適應 viewport,可能會有餘留的空白。slice – 保持縱橫比同時比例小的方向放大填滿 viewport,超出的部分被剪裁掉。none – 扭曲縱橫比以充分適應 viewport。例子1:preserveAspectRatio="xMidYMid meet" 表示 綠色框框 與 显示器的 x 方向、y方向的 中心點 對齊;
例子2:preserveAspectRatio="xMidYMin slice" 表示 綠色框框 與 显示器的 x 方向 中心點 對齊,Y 方向 上邊緣對齊,保持比例放大填滿 显示屏 后超出部分隱藏;
例子3:preserveAspectRatio="xMidYMid slice" 表示 綠色框框 與 显示器的 x 方向、y方向的 中心點 對齊,保持比例放大填滿显示屏 后超出部分隱藏;
例子4:preserveAspectRatio="none" 不管三七二十一,隨意縮放綠色框框,填滿 显示屏即可;這就是非比例縮放的答案了。
微信小程序官方不支持 SVG 標籤的,但是決定曲線救國,相當於自己實現了一個SVG標籤:使用小程序內置的 Canvas 渲染器, 在 Cax 中實現 SVG 標準的子集,使用 JSX 或者 HTM(Hyperscript Tagged Markup) 描述 SVG 結構行為表現。
但是今天我想講講其他的。
我們知道,小程序雖然不支持 SVG 標籤,但是支持 svg 轉成 base64 後作為 background-image 的 url,如 background-image: url("data:image/svg+xml.......) 。
但是我這邊還有個需求,隨時更改 SVG 每個路徑的顏色,即 顏色可配置:
來迴轉 Base64 肯定是比較麻煩的,有沒有更好的方式呢?
直接貼答案:對於SVG圖形,還有更好的實現方式,就是直接使用SVG XML格式代碼,無需進行base64轉換。
直接使用 SVG XML 格式代碼,首先要了解 Data URI的格式。
划重點:base64非必選項,不指定的時候,後面的 <data> 將使用 URL編碼。
百分號編碼(Percent-encoding), 也稱作URL編碼(URL encoding),是特定上下文的統一資源定位符 (URL)的編碼機制。
原理:ASCII 字符 = % + 兩位 ASCII 碼(十六進制)。
例如,字符 a 對應的 ASCII 碼為 0x61,那麼 URL 編碼后得到 %61 。
前言:
Data URI 的格式中的 <data> 完全使用URL 編碼也是可以的,如 encodeURIComponent('<svg version="1.1" viewBox= …</svg>')。
但是和轉義前原始SVG相比,可讀性差了很多,而且佔用體積也變大了。
如果深入了解URL 編碼的話,<data> 沒必要全部編碼的。
正文:
RFC3986文檔規定,URL中只允許包含 未保留字符 以及 所有保留字符。
:/?#[]@ (分隔Url的協議、主機、路徑等組件) 和 !$&'()*+,;= (用於在每個組件中起到分隔作用的,如&符號用於分隔查詢多個鍵值對)。%、空格、雙引號"、尖號 <>等等。綜上所述,只需要對 受限字符或不安全字符 進行編碼即可。
svgToUrl (svgData) {
encoded = encoded
.replace(/<!--(.*)-->/g, '') // 親測必須去掉註釋
.replace(/[\r\n]/g, ' ') // 親測最好去掉換行
.replace(/"/g, `'`) // 單引號是保留字符,雙引號改成單引號減少編碼
.replace(/%/g, '%25')
.replace(/&/g, '%26')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
return `data:image/svg+xml,${encoded}`
}
如果使用在 CSS 中,可利用 SASS版本3.3以上 的 三個API 對 SVG字符串做替換處理。
str_insert(string, insert, index): 從 $string 第 $index 插入字符 $insert;str_index(string, substring): 返回 $substring 在 $string 中第一個位置;str_slice(string, start_at, end_at = nil): 返回從字符 $string 中第 $start_at 開始到 $end_at 結束的一個新字符串。前人已有總結,可前往 https://github.com/leeenx/sass-svg/blob/master/sass-encodeuri.scss 查看完整代碼。
一般從 Sketch 導出 SVG ,冗餘代碼比較多,有條件的話建議使用 SVGO 壓縮SVG的原本體積,比如清除換行、重複空格;刪除文檔聲明;刪除註釋;刪除desc描述等等。
SVG強大的地方在於,出其不意,炫酷,與眾不同。
無論是微信公眾號花式排版,foreignObject 標籤實現截圖,實現非比例縮放,或者 背景圖直接使用 SVG XML 格式代碼,還是上文沒有提及的路徑動畫、描邊動畫、圖形裁剪、濾鏡等等,都可以玩出新的花樣。
SVG 一個屬性可成就一篇文章,學習 SVG 可以說是在挑戰自己,歡迎加入 SVG 的學習隊列。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※新北清潔公司,居家、辦公、裝潢細清專業服務
※教你寫出一流的銷售文案?
1、首先要在 添加好友 這個按鈕上添加一個事件,也就是在detail.wxml的添加好友這個按鈕的哪裡,添加一個點擊事件 handleAddFriend
並且添加好友還要考慮,現在是已登陸狀態還是未登陸狀態的,只有是登陸狀態的時候,才可以發起添加好友的請求的
所以就要先判斷一下它是否已經登陸了
因為只要是登陸之後,就會把用戶的id寫入到全局的userinfo下面的
handleAddFriend(){ if( app.userInfo._id){ } else{ wx.showToast({ title: '請先登陸', duration : 2000, // 然後不要讓它显示圖標 icon : 'none', success: ()=>{ // 如果成功的話就直接跳轉到我的頁面去 // 但是注意了這裏不能用 navigator to,因為它主要是跳轉 // 普通的頁面,而這裏“我的頁面”其實是同tabbar來進行配置的 } }) } }
這個時候就可以查找一下 小程序文檔 中關於“路由”的介紹了
可以看到要用wx.switchtab來進行操作了
然後因為我們設置了那個提示“請先登陸”是維持兩秒鐘,所以我們也要設置這個跳轉到我的頁面中的時間也是兩秒鐘
handleAddFriend(){ if( app.userInfo._id){ } else{ wx.showToast({ title: '請先登陸', duration : 2000, // 然後不要讓它显示圖標 icon : 'none', success: ()=>{ // 如果成功的話就直接跳轉到我的頁面去 // 但是注意了這裏不能用 navigator to,因為它主要是跳轉 // 普通的頁面,而這裏“我的頁面”其實是同tabbar來進行配置的 setTimeout(()=>{ wx.switchTab({ url: '/pages/user/user', }) } , 2000); } }) } }
上面的加入 沒登陸的情況也寫好了,下面就是對已經登陸了之後的設計了
就要在數據可以中建立一個message集合,主要是用來存儲好友消息,或者是系統的消息給這個用戶的一個信息集合的
這個集合裏面的每一個信息,包含了userID也就是這個好友請求或者是信息是發送給哪一個人的
然後還有一個其他想要加他好友的用戶id list數組,因為每個人都可以給這個人發起好友請求的,這就是對於數據庫1的建立了
所以在已經登陸之後,先查看一下有沒有這個發起好友的信息了,如果還有的話,就在數據庫中創立這個字段了
這個數據庫裏面的userid字段存的其實就是我們要加的這個人的id標識了,然後這個人的id我們可以從這個人的詳情頁面(detail)下的data中來獲得的
通過where就可以定位到在數據庫中這個用戶對應的字段了,然後用get就可以開始對這個字段裏面的東西進行查詢了
如果這個信息已經存在了就做更新操作,如果不存在的話就做創建操作即可了
if( app.userInfo._id){ db.collection('mesasge').where({ userId : this.data.detail._id }).get().then((res)=>{ if( res.data.length){//更新 } else{ //tianjia1 db.collection('message').add({ data : { userId : this.data.detail._id, list : [ app.userInfo._id] } }) } }); }
之後就可以查看數據可以中的message 集合
這樣的話,說明就調用成功了
二、更新message 信息
因為如果已經申請過了的話,就不能再往list裏面添加自己的id了,所以就要檢測一下現在是否在list中,
可以直接調用數組的include方法來進行查詢,如果找到了的話,就提示“已申請過了”如果沒找到的話就要往這個數組裡面進行數據更新了
查看了微信開放文檔之後會發現,如果是單個數據進行更新的話可以直接用doc好到之後進行更新就好了,但是如果對大量的數據進行批量的更新的話
因為在客戶端的更新能力還是有限的,所以就要到服務端上來完成了,也就是用雲函數來完成了(其實這個方法我們已經寫好了,也就是雲函數update了
else{ wx.cloud.callFunction({ // name也就是我們要修改的數據庫的名字,data就是在雲函數中 // 想要的參數了 name : 'updata', data : { collection : 'message', where :{ userId : this.data.detail._id }, data : { } } }) }
注意了,在調用雲函數的時候,前面的data和後面的data是不一樣的,錢買你的是給雲函數的參數,但是後面的是我們要修改的數據了
在要修改list的數據到時候,就涉及了要對數組進行添加,也就是push操作了,其實在數據可以中也內置了一些的方法,commend.push等等
注意:在detail.js文件中,如果找到了這個數據流,但是沒有申請的話,就是進行更新,在更新的時候用到了update雲更新函數,
給這個雲函數傳入的data中
data : `{list : _.unshift(' ${app.userInfo._id} ')}`
注意:最外面那層 並不是 單引號,而是 鍵盤 Esc下面的那個標點
三、添加好友功能之監聽message消息
在數據庫加入了一個 帶有list和userid的數據,所以在userid這個人登陸小程序之後,就應該可以看到有沒有人給他發送消息了
並且還是要實時的更新,就是這個人在登陸狀態的話,也可以直接收到了,也就是要實現實時的監聽數據庫中list的實時變化了
在開發者文檔中:雲開發-》實時數據的推送:
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/realtime.html
(它的意思就是我們可以監聽到數據庫發送的變化
可以直接查看demo
const db = wx.cloud.database() const watcher = db.collection('todos') // 按 progress 降序 .orderBy('progress', 'desc') // 取按 orderBy 排序之後的前 10 個 .limit(10) .where({ team: 'our dev team' }) .watch({ onChange: function(snapshot) { console.log('docs\'s changed events', snapshot.docChanges) console.log('query result snapshot after the event', snapshot.docs) console.log('is init data', snapshot.type === 'init') }, onError: function(err) { console.error('the watch closed because of error', err) } }) // ... // 等到需要關閉監聽的時候調用 close() 方法 watcher.close()
我們可以在user頁面中進行檢測即可,也就是在登陸之後進行檢測了
我們創建了一個方法 getMessage()。只要用戶登陸了之後就可以進行觸發了,在onReady裏面的登陸成功代碼之後即可了
也就是在數據庫定位到uuseid是這個用戶的數據之後,得到了之後就可以用watch方法來進行監聽了
getMessage(){ db.collection('message').where({ userId : app.userInfo._id }).watch({ onChange: function (snapshot) { console.log(snapshot); } }); }
這個onChange就是進行監聽的函數了,我們在遇到陌生的一定要傳入參數的函數的時候,最好是把這個參數用console.log打印出來看看我們的想要的數據在哪個位置裏面的
注意了:如果是按照上面這樣的話,是會報錯的,以為缺少了錯誤返回的 onError函數的,示例的demo裏面的格式是怎麼樣的,最好就用怎麼樣的
不然可能都是會報錯的
但是會發現,我們沒拿到有用的數據
就可以用多賬號來調試一下了
(這裏用的多賬號最好還是用真實的賬號把,因為虛擬的出現的問題挺大的)
(然後還要設置給message權限是第一個,允許全部人看的那種,才可以看到在別的賬號上的加好友信息的
在別的賬號上面的話就可以看到打印的信息了,可以看到我們得到的消息其實是挺亂的,所以最好用判斷來搞一下
測試之後會發現,得到的 snapshot 數據中有一個 docChanges 數組的,只要有申請,就會有显示了
所以我們可以通過對數組的長度進行一個判斷
然後再對這個list進行判斷,通過長度來進行判斷,如果有長度的話說明就有消息了
有消息的話就要給用戶一個提示,就是在下面的tabbar中的消息圖標右上角添加一個紅色的小點
===其實這個功能在微信小程序中其實就已經幫我們設計好了
微信文檔-》API-》界面-》tabbar-》wx.showTabBarRedDot
https://developers.weixin.qq.com/miniprogram/dev/api/ui/tab-bar/wx.showTabBarRedDot.html
它需要定義一個index屬性,來指定放紅點的是tabbar中的哪一項的(它是從0開始的,所以我們設置為2即可了)
也就是說這個用戶拿到了這個list之後,通過這個list的長度來判斷有沒有消息,然後設置紅點提示,並且還要把這個得到的list用到消息頁面中去的
所以就涉及到了,怎麼把這個得到的list共享到消息中去,這個和之前的userInfo是類似的,點開全局的app.js
this.userMessage = []
這裏創立的是一個數組來的,不是對象了
然後在user.js裏面,判斷這個得到的list的長度,設置tabbar上面的小紅心,然後把得到的list賦值給全局的userMessage
但是如果檢測到這個list是空的話,就要把在tabbar上面的小紅心取消掉了
**然後還要讓我們全局的userMessage等於一個空的數組即可了
這樣,這個監聽的函數就完成了:
getMessage(){ db.collection('message').where({ userId : app.userInfo._id }).watch({ onChange: function (snapshot) { // console.log(snapshot) if( snapshot.docChanges.length){ //這裏就可以直接拿到message裏面所對應的消息列表了 let list = snapshow.docChanges[0].doc.list; if( list.length ){ wx.showTabBarRedDot({ index: 2, }); app.userMessage = list; } else{ wx.hideTabBarRedDot({ index: 2, }) app.userMessage = []; } } }, onError: function (err) { console.error('the watch closed because of error', err) } }); }
然後因為watch是實時監聽的,我們在數據庫裏面把給的信息刪掉的話
這個紅點也就會消失了
這就是因為正在實時的監聽着
四、下面搞的就是如何把共享的userMessage在消息頁面中渲染出來
===消息頁面和removeList組件布局
首先 切換編譯模式到消息頁面中,先做布局
<view class="message"> <view> <text>暫無消息:</text> </view> <view> <text>消息列表:</text> <view>第一條消息</view> <view>第二條消息</view> </view> </view>
之後就是先判斷有沒有消息。所以就要在js看i嗎添加一個東西,這裏添加的是一個userMessage數組,就是用來接收我們的那個全局的list的,
如果這個數組是空的話,說明就是沒有消息了,反之,所以就可以通過這個來進行判斷了
**之後就要測試一下message這個頁面裏面文件的生命周期了
這就是為了測試,在tabbar中的生命周期是怎麼樣的
在message.js裏面
onReady: function () {
console.log(1)
},
/**
* 生命周期函數--監聽頁面显示
*/
onShow: function () {
console.log(2)
},
在點擊了下main的tabbar的圖標之後,
打印的結果:
但是當我們幾點了個人頁面之後,再點擊消息頁面的時候,只打印了2
也就是說在tabbar裏面的onreay並不會再次的觸發(但是普通頁面的onreay是會被再次觸發的),但是他也是會觸發onshow的
所以基於這個就可以在onshow中添加東西,來監聽現在的消息變化情況了
因為如果想要進入消息頁面的話,就得先登陸,所以這力又要一個判斷了,如果沒登陸得話,就讓他跳轉到登陸頁面去的
(這個功能就和我們的detail裏面很像的,就可以直接COPY過來了)
const app = getApp() Page({ /** * 頁面的初始數據 */ data: { userMessage : [], logged : false }, /** * 生命周期函數--監聽頁面加載 */ onLoad: function (options) { }, /** * 生命周期函數--監聽頁面初次渲染完成 */ onReady: function () { // console.log(1) }, /** * 生命周期函數--監聽頁面显示 */ onShow: function () { // console.log(2) if( app.userInfo._id ){ this.setData({ logged : true, userMessage : app.userMessage }); }else{ wx.showToast({ title: '請先登陸', duration: 2000, // 然後不要讓它显示圖標 icon: 'none', success: () => { // 如果成功的話就直接跳轉到我的頁面去 // 但是注意了這裏不能用 navigator to,因為它主要是跳轉 // 普通的頁面,而這裏“我的頁面”其實是同tabbar來進行配置的 setTimeout(() => { wx.switchTab({ url: '/pages/user/user', }) }, 2000); } }) } }, /** * 生命周期函數--監聽頁面隱藏 */ onHide: function () { }, /** * 生命周期函數--監聽頁面卸載 */ onUnload: function () { }, /** * 頁面相關事件處理函數--監聽用戶下拉動作 */ onPullDownRefresh: function () { }, /** * 頁面上拉觸底事件的處理函數 */ onReachBottom: function () { }, /** * 用戶點擊右上角分享 */ onShareAppMessage: function () { } })
message.js
雖然我們吧list引入進來了,下面就是吧這個list显示出來了
我們用虛擬賬號,給我的主號提交了兩個申請來進行測試了
之後在message.wxml中對我們的信息進行打印:
<!--miniprogram/pages/message/message.wxml--> <view class="message" wx:if="{{ logged }}"> <view wx:if="{{ !userMessage.length }}"> <text class="message-text">暫無消息:</text> </view> <view wx:else> <text class="message-text">消息列表:</text> <view wx:for="{{ userMessage }}" wx:key="{{index}}">{{item}}</view> </view> </view>
我們還要進行優化,就是可以得到一個列表,有頭像有昵稱,等信息的,別還要有刪除的功能的
為了練習一下組件的功能,雖然這個刪除的功能可以直接在這個文件裏面寫,但是我們還是吧這個刪除變成一個組件的形式l
新建一個removeList的刪除組件,然後就是找到message的JSON文件引入這個組件
這個組件其實是可以拖拽的?
在文檔-》組件-》視圖容器 movable-area和movable-view相互配合的
demo:
<movable-area> <movable-view x="{{x}}" y="{{y}}" direction="all">text</movable-view> </movable-area>
我們設置的結構和樣式:
<!--components/removeList/removeList.wxml--> <movable-area class="area"> <movable-view class="view">小喵喵</movable-view> </movable-area>
/* components/removeList/removeList.wxss */ .area{width:800rpx; height: 150rpx; margin: 20rpx ; position: relative;background: blue;} .view{width:630rpx; height:100%; background: red;position: absolute;left:150rpx;top:0; line-height: 150rpx;text-indent: 10rpx;}
效果;
但是這個時候還是不能進行拖拽的,其中的direction定義的就是拖拽的方向
添加了這個屬性之後:
<!--components/removeList/removeList.wxml--> <movable-area class="area"> <movable-view direction="horizontal" class="view">小喵喵</movable-view> </movable-area>
就可以進行拖拽了
注意;下面設置的z-Index是在設置級別,z-index大的會覆蓋在小的上面的
<!--components/removeList/removeList.wxml--> <movable-area class="area"> <movable-view direction="horizontal" class="view">小喵喵</movable-view> <image src="" /> <view class="delete">刪除</view> </movable-area>
/* components/removeList/removeList.wxss */ .area{width:800rpx; height: 150rpx; margin: 20rpx ; position: relative;border-bottom: 1px #cdcdcd dashed} .view{width:630rpx; height:100%; position: absolute;left:150rpx;top:0; line-height: 150rpx;text-indent: 10rpx;z-index: 2;} image{ width: 100rpx; height: 100rpx; border-radius: 50%; position: absolute; left: 0; top: 0; z-index: 1; } .delete{ width: 200rpx; height: 150rpx; background: red; color: white; position: absolute; right: 0; top: 0; z-index: 1; }
但是得到的效果是:
會發現不管怎麼拖拉,都擋不住後面的刪除–原因就是class==view這塊沒加背景色,雖然層級夠了
給他在wxss裏面添加一個 background:white即可了
刪除那個文字要居中的話,
.delete{ width: 170rpx; height: 100rpx; background: red; color: white; position: absolute; right: 0; top: 0; z-index: 1; line-height: 100rpx; }
效果圖:
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※幫你省時又省力,新北清潔一流服務好口碑
※別再煩惱如何寫文案,掌握八大原則!
這個官方文檔一段對MySQL內核分析的一個嚮導。是對MySQL一條insert語句寫入到MySQL數據庫的分析。
但是,對於MySQL 5.7版本來說,基本上都是寫入到innodb引擎。但也還是有借鑒意義,大的框架沒有太大變化。
後面的文檔,會通過mysqld –debug 和gdb等工具,通過分析mysqld.trace來分析insert語句在MySQL 5.7中怎麼寫入數據庫。
官方文檔給出的一段結構,如下:
/sql/mysqld.cc
/sql/sql_parse.cc
/sql/sql_prepare.cc
/sql/sql_insert.cc
/sql/ha_myisam.cc
/myisam/mi_write.c
上述梳理一個過程,是說從客戶段執行一條簡單的insert語句,然後到達MySQL服務器端,並通過MyISAM存儲層。寫入到MyISAM文件的過程。
由於,我們現在的主流都是InnoDB存儲引擎,所以我們分析的寫入到存儲層應該是InnoDB的源代碼。但是上述的一個框架也有借鑒意義。雖然,走的是InnoDB存儲引擎插入數據,但是也還是需要通過SQL層的ha_*這樣的接口進行接入。
正題開始!!!!!!!!!!!!!!!!!!!!!!!
第一步,進入MySQL大門的地方。夢開始的地方。眾所周知,C語言都是需要main方法作為主入口。而MySQL的主入口如下:
代碼位置 /sql/mysqld.cc
int main(int argc, char **argv)
{
_cust_check_startup();
(void) thr_setconcurrency(concurrency);
init_ssl();
server_init(); // 'bind' + 'listen'
init_server_components();
start_signal_handler();
acl_init((THD *)0, opt_noacl);
init_slave();
create_shutdown_thread();
create_maintenance_thread();
handle_connections_sockets(0); // ! 這裏也代表着我們進入下一個門的地方
DBUG_PRINT("quit",("Exiting main thread"));
exit(0);
}
這裏可以看到很多的init_*或者server_init()。通過名字我們可以猜測出,這裏做了很多初始化的工作。例如:啟動過程中一些初始化的檢查和MySQL配置變量的加載和一些組件的初始化等。
這裏重要的函數是handle_connections_sockets
繼續跟蹤 /sql/mysqld.cc
handle_connections_sockets (arg __attribute__((unused))
{
if (ip_sock != INVALID_SOCKET)
{
FD_SET(ip_sock,&clientFDs);
DBUG_PRINT("general",("Waiting for connections."));
while (!abort_loop)
{
new_sock = accept(sock, my_reinterpret_cast(struct sockaddr*)
(&cAddr), &length);
thd= new THD;
if (sock == unix_sock)
thd->host=(char*) localhost;
create_new_thread(thd); // !
}
從簡易的思維,忽視其他的判斷語句。可以看到這裏做的是典型的client/server架構。服務器有一個主線程,它總是偵聽來自新客戶機的請求。一旦它接收到這樣的請求,它將分配資源。特別是,主線程將生成一個新線程來處理連接。然後主服務器將循環並偵聽新連接——但我們將保留它並跟蹤新線程。
這裏創建新線程的方法是:create_new_thread(thd);
繼續跟蹤 /sql/mysqld.cc
create_new_thread(THD *thd)
{
pthread_mutex_lock(&LOCK_thread_count);
pthread_create(&thd->real_id,&connection_attrib,
handle_one_connection, // !
(void*) thd));
pthread_mutex_unlock(&LOCK_thread_count);
}
可以看到這裏獲得一個新線程加入一個互斥鎖,避免衝突。
繼續跟蹤 /sql/mysqld.cc
handle_one_connection(THD *thd)
{
init_sql_alloc(&thd->mem_root, MEM_ROOT_BLOCK_SIZE, MEM_ROOT_PREALLOC);
while (!net->error && net->vio != 0 && !thd->killed)
{
if (do_command(thd)) // !
break;
}
close_connection(net);
end_thread(thd,1);
packet=(char*) net->read_pos;
從這裏開始,我們即將脫離mysqld.cc文件,因為我們獲得了thread,且分配一小段內存資源,給與我們來處理我們的SQL語句了。
我們會走向何方呢,可以開始觀察do_command(thd)方法。
繼續跟蹤/sql/sql_parse.cc
bool do_command(THD *thd)
{
net_new_transaction(net);
packet_length=my_net_read(net);
packet=(char*) net->read_pos;
command = (enum enum_server_command) (uchar) packet[0];
dispatch_command(command,thd, packet+1, (uint) packet_length);
// !
}
其中從這裏可以看到,do_command(THD *thd)把它串聯起來的是一個叫作THD的東西,也就是thread。所以後面的工作和行為,基本都是通過thread進行牽線搭橋的。
my_net_read函數位於另一個名為net_servlet .cc的文件中。該函數從客戶端獲取一個包,解壓縮它,並去除頭部。
一旦完成,我們就得到了一個名為packet的多字節變量,它包含客戶端發送的內容。第一個字節很重要,因為它包含標識消息類型的代碼。
說明了packet第一個字節很重要。debug也有證據進行一個佐證。
packet_header: Memory: 0x7f7fc000a4b0 Bytes: (4)
21 00 00 00
然後把packet第一個字節和餘下的部分傳遞給dispatch_command
繼續跟蹤/sql/sql_parse.cc
bool dispatch_command(enum enum_server_command command, THD *thd,
char* packet, uint packet_length)
{
switch (command) {
case COM_INIT_DB: ...
case COM_REGISTER_SLAVE: ...
case COM_TABLE_DUMP: ...
case COM_CHANGE_USER: ...
case COM_EXECUTE:
mysql_stmt_execute(thd,packet);
case COM_LONG_DATA: ...
case COM_PREPARE:
mysql_stmt_prepare(thd, packet, packet_length); // !
/* and so on for 18 other cases */
default:
send_error(thd, ER_UNKNOWN_COM_ERROR);
break;
}
這裏sql_parser .cc中有一個非常大的switch語句
switch語句中代碼有:code for prepare, close statement, query, quit, create database, drop database, dump binary log, refresh, statistics, get process info, kill process, sleep, connect, and several minor commands
除了COM_EXECUTE和COM_PREPARE兩種情況外,我們刪除了所有情況下的代碼細節。
可以看到
COM_EXECUTE 會調用mysql_stmt_execute(thd,packet);
COM_PREPARE 會調用mysql_stmt_prepare(thd, packet, packet_length);
這裏就像一个中轉站一般,看我們去向什麼地方。這裏去的門是:COM_PREPARE:mysql_stmt_prepare
跟蹤 /sql/sql_prepare.cc
下面是一段prepare的註釋
"Prepare:
Parse the query
Allocate a new statement, keep it in 'thd->prepared statements' pool
Return to client the total number of parameters and result-set
metadata information (if any)"
繼續回到主線COM_EXECUTE
跟蹤/sql/sql_parse.cc
bool dispatch_command(enum enum_server_command command, THD *thd,
char* packet, uint packet_length)
{
switch (command) {
case COM_INIT_DB: ...
case COM_REGISTER_SLAVE: ...
case COM_TABLE_DUMP: ...
case COM_CHANGE_USER: ...
case COM_EXECUTE:
mysql_stmt_execute(thd,packet); // !
case COM_LONG_DATA: ...
case COM_PREPARE:
mysql_stmt_prepare(thd, packet, packet_length);
/* and so on for 18 other cases */
default:
send_error(thd, ER_UNKNOWN_COM_ERROR);
break;
}
現在“COM_EXECUTE 中的mysql_stmt_execute`是我們關注的重點,我們來看看
跟蹤/sql/sql_prepare.cc代碼
void mysql_stmt_execute(THD *thd, char *packet)
{
if (!(stmt=find_prepared_statement(thd, stmt_id, "execute")))
{
send_error(thd);
DBUG_VOID_RETURN;
}
init_stmt_execute(stmt);
mysql_execute_command(thd); // !
}
這裏做一個判斷,看是否是execute,然後初始化語句,並開始執行mysql_execute_command(thd);可以看到,是通過thread來調用動作。
跟蹤/sql/sql_parse.cc代碼
void mysql_execute_command(THD *thd)
switch (lex->sql_command) {
case SQLCOM_SELECT: ...
case SQLCOM_SHOW_ERRORS: ...
case SQLCOM_CREATE_TABLE: ...
case SQLCOM_UPDATE: ...
case SQLCOM_INSERT: ... // !
case SQLCOM_DELETE: ...
case SQLCOM_DROP_TABLE: ...
}
lex 解析sql語句。然後進入SQLCOM_INSERT。
跟蹤/sql/sql_parse.cc代碼
case SQLCOM_INSERT:
{
my_bool update=(lex->value_list.elements ? UPDATE_ACL : 0);
ulong privilege= (lex->duplicates == DUP_REPLACE ?
INSERT_ACL | DELETE_ACL : INSERT_ACL | update);
if (check_access(thd,privilege,tables->db,&tables->grant.privilege))
goto error;
if (grant_option && check_grant(thd,privilege,tables))
goto error;
if (select_lex->item_list.elements != lex->value_list.elements)
{
send_error(thd,ER_WRONG_VALUE_COUNT);
DBUG_VOID_RETURN;
}
res = mysql_insert(thd,tables,lex->field_list,lex->many_values,
select_lex->item_list, lex->value_list,
(update ? DUP_UPDATE : lex->duplicates));
// !
if (thd->net.report_error)
res= -1;
break;
}
對於插入數據,我們要做的第一件事情是:檢查用戶是否具有對錶進行插入的適當特權,服務器通過調用check_access和check_grant函數在這裏進行檢查。
有了權限才可以做【插入】動作。
我們可以導航 /sql 目錄,如下:
Program Name SQL statement type
------------ ------------------
sql_delete.cc DELETE
sql_do.cc DO
sql_handler.cc HANDLER
sql_help.cc HELP
sql_insert.cc INSERT // !
sql_load.cc LOAD
sql_rename.cc RENAME
sql_select.cc SELECT
sql_show.cc SHOW
sql_update.cc UPDATE
sql_insert.cc是具體執行插入的操作。
上面的mysql_insert() 的方法具體實現,在sql_insert.cc文件中。
跟蹤 /sql/sql_insert.cc代碼
int mysql_insert(THD *thd,TABLE_LIST *table_list, List<Item> &fields,
List<List_item> &values_list,enum_duplicates duplic)
{
table = open_ltable(thd,table_list,lock_type);
if (check_insert_fields(thd,table,fields,*values,1) ||
setup_tables(table_list) ||
setup_fields(thd,table_list,*values,0,0,0))
goto abort;
fill_record(table->field,*values);
error=write_record(table,&info); // !
query_cache_invalidate3(thd, table_list, 1);
if (transactional_table)
error=ha_autocommit_or_rollback(thd,error);
query_cache_invalidate3(thd, table_list, 1);
mysql_unlock_tables(thd, thd->lock);
}
這裏就要開始,打開一張表。然後各種檢查,看插入表的字段是否有問題。不行就abort。
然後,開始填充記錄數據。最終調用write_record 寫記錄的方法。
由於write_record 會對應不同的存儲引擎,所以這裡有分支的。我這裏講解兩種
繼續跟蹤/sql/sql_insert.cc
int write_record(TABLE *table,COPY_INFO *info)
{
table->file->write_row(table->record[0]; // !
}
終於,要寫文件了。調用那個存儲引擎呢?看handler.h
/* The handler for a table type.
Will be included in the TABLE structure */
handler(TABLE *table_arg) :
table(table_arg),active_index(MAX_REF_PARTS),
ref(0),ref_length(sizeof(my_off_t)),
block_size(0),records(0),deleted(0),
data_file_length(0), max_data_file_length(0),
index_file_length(0),
delete_length(0), auto_increment_value(0), raid_type(0),
key_used_on_scan(MAX_KEY),
create_time(0), check_time(0), update_time(0), mean_rec_length(0),
ft_handler(0)
{}
...
virtual int write_row(byte * buf)=0;
官方文檔默認調用的是 ha_myisam::write_row
代碼 /sql/ha_myisam.cc
如下:
int ha_myisam::write_row(byte * buf)
{
statistic_increment(ha_write_count,&LOCK_status);
/* If we have a timestamp column, update it to the current time */
if (table->time_stamp)
update_timestamp(buf+table->time_stamp-1);
/*
If we have an auto_increment column and we are writing a changed row
or a new row, then update the auto_increment value in the record.
*/
if (table->next_number_field && buf == table->record[0])
update_auto_increment();
return mi_write(file,buf); // !
}
這些以字母ha開頭的程序是處理程序的接口,而這個程序是myisam處理程序的接口。我們這裏就開始調用MyISAM了。
可以看到這裏調用了mi_write(file,buf);
跟蹤/myisam/mi_write.c
int mi_write(MI_INFO *info, byte *record)
{
_mi_readinfo(info,F_WRLCK,1);
_mi_mark_file_changed(info);
/* Calculate and check all unique constraints */
for (i=0 ; i < share->state.header.uniques ; i++)
{
mi_check_unique(info,share->uniqueinfo+i,record,
mi_unique_hash(share->uniqueinfo+i,record),
HA_OFFSET_ERROR);
}
... to be continued in next snippet
這裡有很多唯一性的校驗,繼續看下面
... continued from previous snippet
/* Write all keys to indextree */
for (i=0 ; i < share->base.keys ; i++)
{
share->keyinfo[i].ck_insert(info,i,buff,
_mi_make_key(info,i,buff,record,filepos)
}
(*share->write_record)(info,record);
if (share->base.auto_key)
update_auto_increment(info,record);
}
這裏就是我們寫入到文件的地方。至此,MySQL的插入操作結束。
路徑為:
main in /sql/mysqld.cc
handle_connections_sockets in /sql/mysqld.cc
create_new_thread in /sql/mysqld.cc
handle_one_connection in /sql/sql_parse.cc
do_command in /sql/sql_parse.cc
dispatch_command in /sql/sql_parse.cc
mysql_stmt_execute in /sql/sql_prepare.cc
mysql_execute_command in /sql/sql_parse.cc
mysql_insert in /sql/mysql_insert.cc
write_record in /sql/mysql_insert.cc
ha_myisam::write_row in /sql/ha_myisam.cc
mi_write in /myisam/mi_write.c
1.進入主函數入口
2.建立socket connection的請求
3.創建一個新的線程
4.處理線程,分配內存資源
5.do_command,是獲取packet第一字節,看做什麼操作,並接受餘下字節。
6.dispatch_command,分發操作,這裏分發的是insert。
7.mysql_stmt_execute,檢查是否為execute,初始化,準備做execute動作。
8.mysql_execute_command ,lex解析SQL語句,進入到SQLCOM_INSERT
9.mysql_insert ,開始做插入操作。調用write_record
10.write_record,準備寫入,看調用哪個存儲引擎,寫入前期準備工作
11.ha_myisam::write_row,ha_myisam進行插入寫入。
12.mi_write,最後做寫入操作。
文獻參考:https://dev.mysql.com/doc/internals/en/guided-tour-skeleton.html
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※新北清潔公司,居家、辦公、裝潢細清專業服務
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案