[譯] 用 Shadow DOM v1 和 Custom Elements v1 實現一個原生 Web Component

  • 原文地址:Make a Native Web Component with Custom Elements v1 and Shadow DOM v1
  • 原文作者:Pearl Latteier
  • 譯文出自:掘金翻譯計劃
  • 本文永久鏈接:github.com/xitu/gold-m…
  • 譯者:newraina
  • 校對者:CoderMing

假如你有一個小表單或者組件要在網站的好幾個地方或者好幾個項目里用,你希望它們都能有統一的樣式和行為,但是,你也希望它們能有些靈活性:也許你的表單需要根據容器元素的不同有各種大小,或者組件要在不同的項目里顯示不同的文字和圖標。你知道你需要什么嗎?你需要一個 web component!

Web components 是可以重用和共享的自定義 HTML 元素。和原生 HTML 元素一樣,它們有屬性,有方法,有事件監聽器,能嵌套,能兼容各種 JavaScript 框架。

怎么樣,是不是很厲害?沒有 jQuery,沒有難以維護的面條代碼,它就是一個良好封裝過的帶 UI 和功能的組件了。

介紹一下 Mini-Form 組件

我們要實現一個叫 “mini-form” 的 web component。(Custom element 的名字必須用小寫字母開頭,并且至少有一個連字符。要了解更多可以閱讀相關標準。)它是一個很簡單的表單組件:讓用戶提交投訴意見,并且能確認是否收到了用戶的輸入(實際上并不真的干什么)。這個組件能自適應它容器元素的大小和標題的長度。它有一個基本的 material design 樣式;你可以給每個組件實例指定顏色主題。組件的代碼托管在 github.com/pearlbea/mi…,在線示例請見這里。

定義 Custom Element

Web components 可以用一些新的 web 標準來實現。其中最重要的是最新修訂過的 Custom Elements 標準。(要了解更多關于新的 Custom Elements V1 標準,可以閱讀 Eric Bidelman 的文章)要創建一個 custom element,我們需要兩個東西:一個定義元素行為的類,以及一個告訴瀏覽器如何關聯 DOM 元素標簽和剛才那個類的定義。新建一個叫 mini-form.js 的文件,把下面的類和定義代碼放進去:

class MiniForm extends HTMLElement {constructor() {super();}
}
window.customElements.define('mini-form', MiniForm);
復制代碼

constructor 里,對 super() 不帶參數的調用必須放在第一行。它會為組件設置正確的原型鏈和 this 的值。(更多信息可以參考 Mozilla Developer Network 關于 super 的文章。)

其他準備工作

新建文件的時候,還要創建:一個 index.html,用來實際引用組件;一個 mini-form-test.html,用來寫測試用例,因為組件是你寫的。先在這兩個文件里寫上基本的 HTML5 樣板代碼。

你還需要一些 polyfill。我們使用的 web 標準非常新,還沒被所有瀏覽器支持,至少到目前為止,polyfill 是必須的。對于我們這個簡單的組件,只需要兩個 polyfill:custom elements 和 shadydom,可以用 Bower 安裝:

bower install --save webcomponents/custom-elements
bower install --save webcomponents/shadydom
復制代碼

把這兩個 polyfills 放在 index.htmlmini-form-test.html 的 head 里,(或者用你習慣的構建工具打包在一起,都行,無所謂。)同時,也要把 mini-form.js 引用進每一個 HTML 文件里。index.html 現在差不多是下面的樣子:

<!doctype html>
<html lang="eng"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"><script src="bower_components/shadydom/shadydom.min.js"></script><script src="bower_components/custom-elements/custom-elements.min.js"></script><script src="mini-form.js"></script></head><body></body>
</html>
復制代碼

注意:shadydom polyfill 要放在 custom elements polyfill 前面。不然,你可能會看到 Element#attachShadow 不存在的報錯。(猜猜我是怎么知道的。)shadow DOM 的其他內容后面再說。

編寫測試用例

在真的開始寫組件之前,我們先寫一些測試。我們要測試這個組件能不能在 DOM 中渲染出一個 div,現在它還通不過測試,畢竟我們的組件還幾乎不存在。不過,一旦我們渲染出了一個 div 元素,我們就能體會到目睹測試通過的樂趣。

測試差不多是這個樣子:

suite('<mini-form>', () => {let component = document.querySelector('mini-form');test('renders div', () => {assert.isOk(component.querySelector('div'));});
});
復制代碼

為了運行測試,我們要用到 Polymer Project 創建的 web component tester 工具。用 NPM 安裝好 web-component-tester 之后,在 mini-form-test.html 文件的 head 標簽里加上 node_modules/web-component-tester/browser.js,polyfills 和 mini-form.js 也應該在頁面上了。

你還要在 body 里加上 mini-form 的實例,就像這樣:

<body><mini-form></mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');test('renders div', () => {assert.isOk(component.shadowRoot.querySelector('div'));});});</script>
</body>
復制代碼

好了,跑測試吧!在命令行中輸入 wct,web component tester 會啟動你安裝的所有瀏覽器運行測試。然后,你會看到一個測試失敗的提示:

? test/mini-form-test.html ? <mini-form> ? renders div expected null to be truthy
復制代碼

如果你遇到了其他問題,可以在這里看看到這一步,你的代碼應該是什么樣子。

編寫模版

現在我們可以來擴充組件的實現并讓測試通過了。

class MiniForm extends HTMLElement {constructor() {super();}connectedCallback() {this.innerHTML = this.template;}get template() {return `<div>This is a div</div>`;}
}
復制代碼

上面的代碼新增了一個返回最簡單模板的 getter。然后,在 connectedCallback 中,模板賦給了組件的 innerHTML。connectedCallback 方法是custom element 生命周期的一部分,當組件插入到 DOM 中時會被調用。

再跑一遍測試,噢耶!這次肯定能通過!當然,這個組件最后不會僅僅只顯示一個 div。我們要寫更多的測試,看著它們測試失敗,再靠代碼實現讓它們最終都能通過。

// mini-form-test.html
test('renders input', function() {assert.isOk(component.querySelector('input[type="text"]'));
});test('renders button', function() {assert.isOk(component.querySelector('button'));
});// mini-form.js
get template() {return `<div><input type="text" name="complaint" /><button>Submit</button></div>`;
}
復制代碼

增加樣式和 Shadow DOM

到目前為止,mini-form 組件還不是很好看,是時候加一點樣式了。不管用在哪里,組件的樣式都應該在所有的實例間保持統一。我們并不希望組件所在頁面的 CSS 或者 JS 會影響到組件,也不希望組件的樣式或行為影響到了它所處的頁面。可以通過把組件的內容封裝在 Shadow DOM 里來實現這一點。

Shadow DOM 和你早已熟悉和喜愛的 DOM 很像。它有相同的樹形結構和工作方式,只是:它不會和父級 DOM 相互影響;也不會成為它所附屬元素的子元素。

我們要修改 mini-form 來讓它支持 Shadow DOM。

connectedCallback() {this.initShadowDom();
}initShadowDom() {let shadowRoot = this.attachShadow({mode: 'open'});shadowRoot.innerHTML = this.template;
}
復制代碼

我們不再把模板內容直接賦給組件自身的 innerHTML,而是創建一個 shadowRoot 作為中介:給組件關聯上一個 Shadow DOM,然后把模板內容賦給這個 Shadow DOM 的 innerHTML。

這樣做會破壞掉所有的測試,不過,改起來也很簡單,只要在 DOM 查詢上加上剛定義過的 shadowRoot 即可。

test('renders div', () => {assert.isOk(component.shadowRoot.querySelector('div'));
});
test('renders input', () => {assert.isOk(component.shadowRoot.querySelector('input'));
});
test('render button', () => {assert.isOk(component.shadowRoot.querySelector('button'));
});
復制代碼

跑一遍測試,確保全都通過之后,我們來加上 Material Design 的樣式。

<style>@import 'https://fonts.googleapis.com/icon?family=Material+Icons';@import 'https://code.getmdl.io/1.3.0/material.indigo-pink.min.css';@import 'http://fonts.googleapis.com/css?family=Roboto:300,400,500,700';.mdl-card {width: 100%;}.mdl-button {margin-top: 10px;}i {margin-right: 5px;}
</style>
<div class="mdl-card mdl-shadow--2dp"><header class="mdl-layout__header"><div class="mdl-layout__header-row"><i class="material-icons">mood_bad</i><div class="mdl-layout-title">complaint box</div></div></header><div class="mdl-card__supporting-text"><input type="text" class="mdl-textfield__input" /></div><div class="mdl-card__actions"><button class="mdl-button mdl-button--raised mdl-button--accent">Submit</button></div>
</div>
復制代碼

在瀏覽器里打開組件的 index.html 看一下,頁面雖然還需要打磨,但是已經有一個好看的輸入框和一個漂亮的粉色按鈕了。

(沒看到粉色按鈕?可以來這里看下到這一步,代碼應該是什么樣子。)

在內部 DOM 中創建 <slot>

Shadow DOM 有個很棒的特性:<slot> 元素,它讓組件可以把它實際的子元素插入到內部結構中。這個能力讓 web components 變得異常靈活。<slot> 元素扮演了一個占位符的角色,使用組件的人可以自己填充內容。對于我們這個組件來說,我們將用 slot 讓我們自己(或者組件未來的用戶)有能力為表單每一個實例提供不同的文字提示或者問題。第一步,先寫好測試:

<body><mini-form>What?!</mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');...test('renders prompt', () => {let index = component.innerText.indexOf('What?!');assert.isAtLeast(index, 0);});});</script>
</body>
復制代碼

上面的測試檢查了 <mini-form> 標簽之間的文本內容是不是在組件中顯示出來了。運行一下測試,可以看到測試失敗了。

為了讓測試通過,在模板中加一個 <slot>

<div class="mdl-card mdl-shadow--2dp"><div class="mdl-card__supporting-text"><h4><slot></slot></h4><input type="text" rows="3" class="mdl-textfield__input" name="prompt" /></div>...
</div>
復制代碼

再跑一遍測試,這次通過了!試試在 index.htmlmini-form 標簽之間寫點東西,然后在瀏覽器里看一下效果。到這一步的代碼在這里。

實現主題化

組件需要能允許我們為每一個實例指定一個顏色主題。為了讓主題化和我們在用的 material design CSS 配合得好,用戶能用的主題會被限制在這里列出的幾種里。我們給組件新增一個 theme 屬性,用戶設置一個字符串值來指定主題。

給這個新特性寫點測試。

<body><mini-form theme="blue-green">What?!</mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');...test('applies color theme to button', () => {let button = component.shadowRoot.querySelector('button');let buttonColor = window.getComputedStyle(button).getPropertyValue('background-color');assert.equal(buttonColor, 'rgb(105, 240, 174)');});test('applies color theme to header', () => {let header = component.shadowRoot.querySelector('header');let headerColor = window.getComputedStyle(header).getPropertyValue('background-color');assert.equal(headerColor, 'rgb(33, 150, 243)');});});</script>
</body>
復制代碼

跑一遍測試,確定一下它們通過沒有。沒通過吧?很好。修改組件的代碼來獲取和使用 theme 屬性。

get theme() {return this.getAttribute('theme') || 'indigo-pink';
}get template() {return `<style>@import 'https://code.getmdl.io/1.3.0/material.${this.theme}.min.css';...</style>...`;
}
復制代碼

我們從 <mini-form> 標簽上獲取 theme 屬性,把它或者它的默認值 indigo-pink 用在 CSS 的地址里。如果我們給 theme 屬性賦了這個 CSS 類庫實際并沒有的主題值,CSS 的地址就不會生效,組件就會很難看。解決這個問題需要寫的代碼(和它的測試用例!),我打算交給你自己來完成。

跑一下測試,哎呀,并沒有全部通過。因為 Firefox 不支持 Shadow DOM,在 Firefox 里跑的測試失敗了。我們已經用上了 shadydom polyfill,但它并不支持 CSS 封裝,有另一個叫 shadycss 的 polyfill 能解決這個問題。跟上面一樣,之后你自己完成。

index.html 里,給 mini-form 標簽增加一個 theme 屬性。然后你就能在瀏覽器里看到你的藝術創作了。

處理事件

組件已經很好看了,但還什么都干不了。我們要干的最后一件事情,是給它加上事件處理的邏輯。當用戶點擊“Submit”按鈕的時候,得發生點什么事情。代碼要獲取輸入,顯示一個成功或失敗(如果輸入為空)的提示。當用戶接著聚焦進輸入框的時候,錯誤信息需要消失掉。

給這些事件邏輯寫上測試。

let input = component.shadowRoot.querySelector('input[type="text"]');
let button = component.shadowRoot.querySelector('button');
let errorMsg = component.shadowRoot.querySelector('.error');test('displays an error message on submit', () => {button.click();let index = errorMsg.innerText.indexOf('Don\'t you have something to say?');assert.isAtLeast(index, 0);
});
test('clears error message on focus', () => {input.focus();let index = errorMsg.innerText.indexOf('Don\'t you have something to say?');assert.isAtLeast(index, -1);
});
test('displays a success message on submit', () => {input.value = 'Some text';button.click();let index = component.shadowRoot.querySelector('.mdl-card').innerText.indexOf('Thank you.');assert.isAtLeast(index, 0);
});
復制代碼

在組件代碼里,給用戶會與之發生交互的兩個元素:輸入框和按鈕綁定事件監聽器。

當用戶聚焦進輸入框,我們希望清空可能在顯示的任何錯誤提示。首先,在模板里新增一個錯誤提示,并且創建一個帶有 visibility: hidden 屬性的 CSS 類 hide

<div class="mdl-card__supporting-text"><h4><slot></slot></h4><input type="text" rows="3" class="mdl-textfield__input" name="question" /><div class="error hide">Don't you have something to say?</div>
</div>
復制代碼

給輸入框綁定一個事件監聽器,處理它的聚焦事件。

connectedCallback() {this.initShadowDom();this.addFocusListener();
}
get input() {return this.shadowRoot.querySelector('input');
}
get errorMessage() {return this.shadowRoot.querySelector('.error');
}
addFocusListener() {this.input.addEventListener('focus', e => {this.hideErrorMessage();});
}
hideErrorMessage() {this.errorMessage.className = 'error hide';
}
復制代碼

上面的代碼給輸入框元素創建了一個 getter、一個在 connectedCallback 里調用的綁定聚焦事件監聽的方法、還有一個在事件監聽中用來隱藏錯誤提示的方法。

接著,給按鈕增加點擊事件的事件監聽和處理點擊的邏輯。

connectedCallback() {this.initShadowDom();this.addFocusListener();this.addClickListener();
}
get button() {return this.shadowRoot.querySelector('button');
}
get card() {return this.shadowRoot.querySelector('.mdl-card');
}
get message() {// this could be a separate component and probably should be if you make it more complicatedreturn `<div><div class="mdl-card__title"><h4>Thank you.</h4></div><div class="mdl-card__supporting-text">We have received your complaint.</div><div class="mdl-card__actions"></div></div>`;
}
addClickListener() {this.button.addEventListener('click', e => {this.getUserInput();});
}
getUserInput() {this.input.value.length > 0 ? this.handleSuccess() : this.displayErrorMessage();
}
handleSuccess() {// You could call a method to save the user's answer herethis.displaySuccessMessage();
}
displaySuccessMessage() {this.card.innerHTML = this.message;
}
displayErrorMessage() {this.errorMessage.className = 'error';
}
復制代碼

跑一遍測試,看它們是不是全都通過!也有可能只是大部分通過:在 Firefox 里,樣式的測試用例依然會失敗。恭喜,你有一個能工作的 web component 了!

全部的代碼在這里。

還可以做很多很多事情來完善和擴展這個組件。除了我早就提到過的,你還可以給頭部標題的文本、圖標加上 slot,或者美化、保存用戶的輸入內容。

覺得還不夠的話,可以寫一個你自己的組件,在 Twitter 上私信給我。祝編程愉快!

相關鏈接

  • webcomponents.org,關于 web components 最重要的信息來源
  • Web Components v1 — the next generation Google 的 Web 更新動向,Taylor Savage 編寫
  • Custom Elements v1: Reusable Web Components Google 的 Web 基礎知識,Eric Bidelman 編寫
  • Shadow DOM v1: Self-Contained Web Components Google 的 Web 基礎知識,Eric Bidelman 編寫
  • Custom Elements That Work Anywhere Rob Dodson 編寫
  • Polymer,一個 web component 庫
  • Skate,也是一個 web component 庫
  • web-component-tester,一個測試 web components 的工具
有任何問題或想法,都可以在 twitter @bendyworks 或者 Facebook 上聯系我們。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改并 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、后端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/252778.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/252778.shtml
英文地址,請注明出處:http://en.pswp.cn/news/252778.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php 原生文件下載

1.整個網頁的html界面源碼下載: xiazai.php <html> <head> <meta charset "utf-8"> <title></title> </head> <body> <form method"post" action"xiazai.php"> <input type"submit&quo…

紅外線攝像機的選擇與使用及原理

紅外線攝像機的選擇與使用及原理 用戶使用紅外燈首先要仔細閱讀使用說明書&#xff0c;特別是為保證人身設備安全的注意事項。檢查前面所講述的配套性方面是否達到要求&#xff0c;應考慮到的影響因素是否考慮到&#xff0c;如未達到要求&#xff0c;可及時調整所用器材。 紅…

asp 之 讓實體中字段類型為DateTime的字段僅僅顯示日期不顯示時間

在我們平時的工作開發中。我們一般會遇到這種一個問題&#xff1a;某個實體的某個字段是DateTime類型的&#xff0c;但是我們在界面上僅僅想讓它顯示日期不顯示時間&#xff01;一個訂單實體&#xff1a;//訂單類public class order{//訂單IDpublic int id{get;set;}//物品IDpu…

JQ的異步文件上傳

一,view代碼 <form role"form"><div class"form-group"><label for"keyinput">選擇文件&#xff1a;</label><input type"file" name"upfile" id"upfile" /></div><div c…

紅外成像與微光成像的區別

在現有的安防技術中,微光和紅外成像是運用最廣的夜視技術.而微光成像主要運用在反恐偵查,部隊作戰的夜視儀中、而紅外夜視成像主要用于監控攝像機的夜間監控較多.   微光成像技術微光夜視技術又稱像增強技術&#xff0c;是通過帶像增強管的夜視鏡&#xff0c;對夜天光照亮的微…

實體類和數據表的映射異常(XXX is not mapping[ ])

在使用SSH框架開發過程&#xff0c;使用hibernate框架提供的工具類實現與數據庫數據交互&#xff0c;在執行cmd操作時&#xff0c;如果出現以下異常&#xff1a; org.hibernate.hql.ast.QuerySyntaxException: xxx is not mapped [from xxx] 或者 nested exception is org.hibe…

Linux下配置LVM

1 LVM介紹LVM(Logical Volume Manager)邏輯卷管理&#xff0c;它是Linux環境下對磁盤分區進行管理的一種機制&#xff0c;LVM是建立在硬盤和分區之上的一個邏輯層&#xff0c;來提高磁盤分區管理的靈活性。通過LVM系統管理員可以輕松管理磁盤分區&#xff0c;邏輯卷管理器的技術…

Python3 配置文件(configparser)(轉載)

本文由 Luzhuo 編寫,轉發請保留該信息. 原文: http://blog.csdn.net/rozol/article/details/72793304 以下代碼以Python3.6.1為例 Less is more! configparser 可以讀寫和解析注釋文件, 但是沒有寫入注釋的功能 1 #!/usr/bin/env python2 # codingutf-83 __author__ Luzhuo4 _…

激光攝像機的原理及應用

近年來&#xff0c;在安防監控領域&#xff0c;以目前視頻監控技術的發展情況&#xff0c;室內監控和白天正常環境下的監控已不是難題&#xff0c;但社會環境的發展日新月異&#xff0c;城市的發展、森林資源的不斷流失、大型項目的建設、邊防安全的守護等&#xff0c;這些環境…

Object.defineProperty 詳解

最近想了解一下Vue是怎么實現數據雙向綁定的&#xff0c;了解到是基于Object.definProperty,在此記錄一下。 Object.defineProperty  顧名思義&#xff0c;就是給對象定義一個屬性&#xff0c;總共有這么幾種&#xff1a; value  屬性的值writable  是否可改寫&#xff0…

Java 實現排序

public class Sort {public static void main(String[] args) {int data[] {43,54,123,5,98,10,7,74,5,54};System.out.println("原先數組&#xff1a;");for(int d : data) {System.out.print(d " ");}System.out.println("\n");/*System.ou…

相機幀率和曝光時間的關系

文章轉載自&#xff1a;http://blog.163.com/pluto_918/blog/static/203853902012111255634175/ 工業相機參數之幀率相關知識詳解&#xff1a; 工業相機是機器視覺系統的重要組成部分之一&#xff0c;在機器視覺系統中有著非常重要的作用。工業相機已經被廣泛應用于工業生產線…

班長的選舉

/* Note:Your choice is C IDE */ #include "stdio.h" #include "string.h" void main() {int zs,ls,ww,zl;//定義一個給參選人員的票數int max;//int xuhao;//char name[5];//參選人員的名字zslswwzl0;//初始票數都為0printf("候選人名單如下\n"…

jquery刷新頁面

window.location.reload()刷新當前頁面. parent.location.reload()刷新父親對象&#xff08;用于框架&#xff09; opener.location.reload()刷新父窗口對象&#xff08;用于單開窗口&#xff09; top.location.reload()刷新最頂端對象&#xff08;用于多開窗口&#xff09; 轉…

Python 常量

總結&#xff1a;在Python中實際中沒有嚴格的常量&#xff1b;知識程序員中約定俗成用變量名全部大寫代表常量 常量的定義&#xff1a; 常量即指不變的量&#xff0c;如pai 3.141592653..., 或在程序運行過程中不會改變的量 舉例&#xff0c;假如老男孩老師的年齡會變&#xff…

SlickOne 敏捷開發框架介紹(二) -- 多用戶/多租戶/SAAS軟件基礎框架實現

前言&#xff1a;在應用于集團版客戶或SAAS平臺服務的業務系統中&#xff0c;流程管理系統需要支持多用戶組織模型。其中包括角色數據、流程定義數據和流程實例數據的多用戶標識綁定。本文旨在全面描述如何基于SlickOne敏捷開發框架實現上述基礎服務功能&#xff0c;形成一個完…

工業相機行曝光與全局曝光

工業相機行曝光與全局曝光 逐行曝光&#xff1a; 圖1 逐行曝光模式 逐行曝光sensor 實現如圖1逐行曝光模式所示。與全局曝光不同&#xff0c;逐行曝光從第一行開始曝光&#xff0c;一個行周期之后第二行才開始曝光。依次類推&#xff0c;經過N-1 行后第N 行開始曝光。第一行曝光…

【Alpha階段匯總】成果展示與體驗總結

一、燃盡圖 二、軟件截圖 三、代碼與圖片、音樂素材倉庫 git倉庫 四、問題與總結 1.git提交問題 之前創建的倉庫地址是http://git.oschina.net/8265559926/groupnet14 但是無論怎么輸入都說找不到倉庫 經反復思考&#xff0c;感覺可能是因為地址不是純字母的原因。就重新注冊了…

Accusoft結構化工具包FormSuite for Structured Forms常見問題解答(二)

FormSuite for Structured Forms是結構化的表單處理SDK和字符識別工具套包&#xff0c;包括表單處理工具FormFix和字符識別工具SmartZone。所有表格處理控件被設計為可以通過內存到內存的數據傳輸模式進行相互溝通。本文收集了一些FormSuite for Structured Forms常見問題及解答…

構建之法閱讀筆記二

構建之法閱讀筆記之二&#xff08;4-11章節&#xff09; 在這之前我不注重代碼的規范性&#xff0c;也不是很注重代碼風格的規范&#xff0c;也沒有對自己寫的代碼進行審查的過程 根據書中所講的這樣寫即使是自己的能力很強&#xff0c;寫的代碼只有自己可以看懂&#xff0c;別…