改变我人生的前端实践
大家好!我叫 Hunter Miller,从事 Web 软件开发大约六年了。一路走来,我尝试过很多方法来开发高效的软件,有些成功,有些则不然。我收集了一些我认为最有影响力的案例,或许可以帮你快速入门。虽然我使用“较大”的模式也取得了一些成功,但我认为较小的模式更强大,整体质量也更好。
这绝不是一份详尽的清单。另外,请原谅其中一些例子,因为大部分都是我编的。😂
一般的
学习/重新学习基础知识永远不会太远
尤其是对于初学者来说,能够立即上手、开始构建并运行起来,感觉棒极了。然而,如果不学习基础知识,你经常会发现自己不太清楚某些事情发生的原因,从而导致在修复 bug 时头疼不已。基础知识可以帮助你缩小问题范围,以便将问题拼凑起来。对于 HTML 来说,这意味着学习基本标签以及何时使用它们。对于 CSS 来说,这意味着了解所有影响布局的不同属性以及它们如何相互作用。对于 JS 来说,这意味着this
始终了解什么是 😂。
尽可能选择最简单的技术
在构建 Web 体验时,我尝试先使用 HTML,然后是 CSS,最后在必要时使用 JS。在使用 JS 之前,HTML + CSS 可以让你走得更远。通常,不必在 JS 中处理此类交互会更简单,性能也更高。
阅读文档和源代码(尤其是 MDN 网页)
文档和源代码是了解事物真正运作方式的门户。每次你花时间阅读文档/源代码,你都会获得以前从未注意到的新知识。网络上有很多你可能不知道的功能。如果你只阅读博客、教程和 Stack Overflow,你永远无法充分发挥你正在使用的工具的潜力。这对于开源库尤其有用,因为作者并不总是有时间撰写文档。这让我成为了一名更优秀的程序员。
无障碍设施必不可少
如果没有适当的无障碍功能,您的网站将无法供那些需要使用屏幕阅读器、高对比度文本、键盘导航等的用户使用。花时间学习浏览器的无障碍功能,将为您下次尝试打造新体验奠定坚实的基础。我发现,在学习无障碍知识的过程中,以下人士和组织提供了很大的帮助:
了解浏览器如何加载资源
这个问题比较棘手,因为浏览器对于不同类型资源的加载优先级并非总是透明的。不过,你可以从以下人员和组织那里学到很多关于性能的知识:
CSS
原子或实用优先 CSS
原子 CSS 是一种构建 CSS 类的方法,它模仿 CSS 中可用的属性和值。乍一听,这听起来有点不可思议。CSS 中有大量属性,每个属性都有多个甚至无限个值,那么这怎么可能行得通呢?首先,你实际上并不需要为每个值都创建一个类。对于那些带有数值的属性,我们制定了一套标准来使用,这样才能最大程度地发挥我们的优势。
例如,我们可能有一组用于各种垂直边距的类。它们可能是my-0 my-1 my-2 my-3
……每个数字代表某个“级别”的边距。例如, my-1
equalsmargin-top: .25rem; margin-bottom: .25rem;
之后的每个级别都只是 + .25rem。现在,我们有了一组非常有用的类,几乎可以应用我们需要的任何垂直边距,除非是极其特殊的情况,否则我们不再需要在 CSS 中编写边距。不仅如此,我们还为每个使用此功能的用户提供了一组标准值供选择。第三个好处是,我们确切地知道类的作用,无需猜测或隐藏值。最后,我们现在为任何元素或组件提供了更改边距的可能性(如果出现情况),而无需在 CSS 中进行特定的覆盖。这可能是我遇到的最强大的 CSS 模式,甚至比 LESS/SASS/SCSS 还要强大。
如果您想了解更多信息,这里有一些很棒的链接,例如《为实用优先 CSS 辩护》、《Tailwind CSS》、《Tachyons CSS 》以及链接至《原子 CSS 案例》的大量文章。
一致排序的属性
这确实很小,并且对你的 CSS 没有实际影响(除非是为了回退不受支持的属性而重复属性)。但是,我发现在阅读/编写 CSS 时保持一致的顺序非常有帮助。我推荐的顺序是布局/定位、大小、间距、排版、颜色/视觉效果、过渡/动画,最后是“杂项”。在每个类别中,我也尽量保持顺序。这有助于发现错误并加快速度。
单类特异性
在深入研究原子 CSS 之前,我嵌套了太多 SASS,这对我来说是一次艰难的经历。我当时在一家设计机构工作,负责构建精品网站,所以一切都非常有创意且没有系统性,或者至少每个组件都有很多例外 😉。我很快发现,覆盖以前编写的 CSS 变得越来越困难,因为所有内容的特殊性各不相同。通过将所有 CSS 扁平化到尽可能接近一个类的特殊性级别,覆盖类上的属性变得容易得多。这与原子 CSS 密切相关,因为只要您的实用程序类放在最后(或者应用了重要类,因为让我们面对现实,如果您将该类放在您想要的位置),它就会覆盖组件类。这可以大大减少特殊性之争。
高性能过渡和动画
这个很简单,但在实际操作中可能会很烦人。CSS 中只有两个属性能够可靠地提供性能,transform
即 和opacity
。其他所有属性都可能执行更昂贵的操作,从而导致卡顿。通常情况下,我尽量只使用这两个属性,但在一些影响较小的场景(例如链接或按钮)中,我仍然会使用color
、background-color
和border-color
。点击此处了解更多关于高性能动画的信息。
HTML
语义,又称学习基本标签
HTML 元素非常多。了解所有元素以及它们的作用并非易事,但它们能帮助您打造更易访问的网络体验,甚至可能减少 CSS 和 JS 代码的编写。合适的 HTML 还能帮助您的网站被搜索引擎抓取,从而提高内容的搜索量。它们是网络的基本组成部分,正确使用它们有助于您创建正常运行的网站。
响应式图像
这是节省图片字节数的最佳方法。响应式图片允许你通过提供通道来描述不同尺寸的图片以及何时应用这些尺寸,从而为用户提供最合适的尺寸。MDN 有一篇很棒的文档对此进行了解释。
<input> + <label> = 交互(<dialog>、<details> 等)
默认浏览器元素中内置了许多交互功能。我们可以利用这些功能构建无需 JavaScript 的交互元素。由于它们不需要 JavaScript,因此更加健壮。但有时,为了真正使一组元素可用且易于访问,JavaScript 是必不可少的。在这种情况下,我们会从 HTML+CSS 入手,然后再添加 JavaScript 所需的附加功能。
<input>
我们可以使用+组合来构建自定义切换、文件输入和许多其他类型的元素,<label>
只需使用:checked
选择器来更新可见元素的状态即可。
弹出框和可折叠元素也可以使用 detail 元素构建。关于此主题的精彩演示:Mu-An Chiou 的《The Details on Details》以及同一位作者关于模态框/对话框的类似演示《A Dialog on Dialogs》。
JS
状态和特殊属性作为字符串(或摆脱布尔成瘾)
在编写 JavaScript 时,我们一开始倾向于将状态写成一组布尔标志。我们可能只是从一个状态开始,isError
然后为其分配一个布尔值。如果这是我们跟踪的唯一状态,那么一开始这样做可能没问题。但很快我们可能会添加更多状态,也许我们需要一个isInProgress
状态。现在我们有两个标志来表示整个组件实际上应该只有一个状态。如果这两个状态都是true
,那么我们应该处于什么状态?我们很快就会遇到可能出现无效状态的情况。
解决这个问题最简单的方法是用字符串来跟踪状态。这个字符串应该是一个有限集合的一部分,这个集合代表了组件可能处于的所有状态。这有助于清理isX
组件中杂乱的属性,我们只需使用一个返回布尔值的函数来检查状态即可。
以下是通过 ajax 调用服务器来加载一些数据的基本示例:
const loaderStates = {
initial: 'initial',
loading: 'loading',
success: 'success',
error: 'error'
}
let state = loaderStates.initial;
function is(status) {
return Array.isArray(status)
? status.indexOf(state) > -1
: state === status;
}
async function loadTodos() {
state = loaderStates.loading;
try {
const todos = await fetch('/todos').then(response => response.json());
state = loaderStates.success;
} catch (error) {
state = loaderStates.error;
console.log(error);
}
}
您可以看到,我们创建了一个名为 的对象来保存状态loaderStates
。我们还创建了一个函数,用于检查当前状态是否与我们想要了解的状态匹配,这相当于我们之前的isError
和is(loaderStates.error)
。现在,我们可以使用此函数在加载时触发旋转效果,或在处于错误状态时触发错误消息。
这种模式的更高级版本称为状态机,您可以从 David K Piano那里了解更多信息,他使用了自己编写的库xstate。
同样,我们倾向于使用这类布尔标志来描述诸如 、 等属性,CanUpdate
但这CanEdit
同样难以维护。与上述方法类似,我们可以改用字符串,以便更简洁地表示某些属性集。这还为我们带来了额外的好处,即能够使用数组函数遍历字符串集,而之前我们可能需要使用冗长的 if-else 语句来检查所有属性。
const admin = {
id: 1,
name: 'Hunter Miller',
canEdit: true,
canPublish: true,
canManageUsers: true,
canApprove: true,
canViewOtherUsersContent: true,
canAccessBackend: true
}
对比
const admin = {
id: 1,
name: 'Hunter Miller',
permissions: [
'edit',
'publish',
'manage-users',
'approve',
'view-others-content',
'access-backend'
]
}
制作地图
在 JS 中,我们经常会遇到一个问题:我们需要根据某个键来执行某些功能或获取数据。与其使用一连串的 if 语句或带有 case 的 switch 语句,不如使用一个对象将特定键与我们需要的内容关联起来,然后使用 即可访问我们需要的内容map[key]
。
我们正在处理的数据:
const transaction = {
id: 88,
price: 33.93,
loginId: 'hmillerdev',
email: 'hunter@hmiller.dev',
paymentId: 'e2b7291d-8838-4435-942a-ec6bec938673'
paymentType: 'credit-card'
};
使用 ifs:
function processTransaction(transaction) {
...
if (transaction.paymentType === 'credit-card') {
processCreditCard(transaction);
} else if (transaction.paymentType === 'paypal') {
processPaypal(transaction);
} else if (transaction.paymentType === 'bank') {
processBank(transaction);
} else if (transaction.paymentType === 'bitcoin') {
processBitcoin(transaction);
} else if (transaction.paymentType === 'seashells') {
processSeashells(transaction);
}
...
}
现在有一张地图:
const processors = {
'credit-card': processCreditCard,
paypal: processPaypal,
bank: processBank,
bitcoin: processBitcoin,
seashells: processSeashells
}
function processTransaction(transaction) {
...
const processPayment = processors[transaction.paymentType];
processPayment(transaction);
...
}
功能化
这一点对我来说非常重要。我学到的第一件事就是尽可能地将函数拆分得更小,以便以后可以复用这部分代码。这样做还有一个额外的好处,那就是现在我们给这部分功能起了个名字。这样,当我们阅读代码时,就能更好地理解它的含义。这也让我们能够利用 JS 提供的一个主要特性:传递函数来修改其他函数的功能。
最好的例子之一就是数组函数map
。我们从数组中调用此函数,并向其传递一个函数,该函数将转换数组中的每个项目并返回一个全新的数组。
const itemPrices = [25.00, 10.00, 48.00];
function priceWithHalfOffDiscount(price) {
return price * 0.5;
}
const discountedPrices = itemPrices.map(price => priceWithHalfOffDiscount(price));
所以现在discountedPrices
等于[12.50, 5.00, 24.00]
。
每当我们需要转换数组中的每个元素并返回转换后的数组时,我们都可以结合使用 map 和任何我们需要的转换函数。现在,我们只用一个函数就拥有了强大的功能,因为我们可以赋予它一些参数来改变它的功能。
这只是函数式编程风格的冰山一角,你可以从 Mattias 在 Fun Fun Function 的函数式编程播放列表中学习,也可以从这本名为《Frisby 教授的函数式编程基本指南》的有趣在线小册子中学习。我发现这种编程风格极大地提高了我的代码质量和弹性,并使很多问题更容易思考。然而,初学时可能会非常困惑,因为这对大多数人来说需要一点思维上的转变。
不要重复自己(太多)
与上一节相关,如果我们有两段非常相似的代码,将它们合并成一个函数并让参数决定差异会很有帮助。如果我们试图将差异太大的代码强行合并在一起,有时可能会带来麻烦。这可能会导致我们创建一个包含过多分支路径或长参数列表的函数。但我发现这种情况并不常见。实际上,我们经常会遇到大量非常相似的东西,一旦我们开始对代码中的某一部分进行去重,就会导致代码的大量修改,从而使代码更加简洁易读。一个好的经验法则是,如果这能帮助代码更一致地处理问题并提高可读性,那么现在是删除冗余代码的好时机。这可能是一把双刃剑,但如果不过度使用,则大多是有益的。
function processOrderWithShipments(data) {
if (!data) {
return;
}
const order = new Order(data);
const shipments = assignItemsToShipments(order.items);
const paymentProcessor = new PaymentProcessor();
if(!order.isValid()) {
throw new Error("Order is invalid");
}
const receipt = paymentProcessor.pay(order.paymentMethod, order.Total);
sendOrderConfirmationEmail(order, receipt);
const shipments = assignItemsToShipments(order.items);
alertFulfillment(shipments);
}
function processRecurringDigitalGoodsOrder(orderData, timePeriod) {
if (!data) {
return;
}
const order = new Order(data);
const paymentProcessor = new PaymentProcessor();
if(!order.isValid()) {
throw new Error("Order is invalid");
}
const receipt = paymentProcessor.pay(order.paymentMethod, order.Total);
sendOrderConfirmationEmail(order, receipt);
setupSubscription(order.paymentMethod, order.items, timePeriod);
sendGoods(order.goods);
}
对比
function processOrder(order) {
const paymentProcessor = new PaymentProcessor();
if(!order.isValid()) {
throw new Error("Order is invalid");
}
const receipt = paymentProcessor.pay(order.paymentMethod, order.Total);
sendOrderConfirmationEmail(order, receipt);
}
function processOrderWithShipments(data) {
if (!data) {
return;
}
const order = new Order(data);
const shipments = assignItemsToShipments(order.items);
try {
processOrder(order);
alertFulfillment(shipments);
} catch (error) {
console.log("😭😭😭😭", error);
}
}
function processRecurringDigitalGoodsOrder(orderData, timePeriod) {
if (!orderData || !timePeriod) {
return;
}
const order = new Order(orderData);
try {
processOrder(order);
setupSubscription(order.paymentMethod, order.items, timePeriod);
sendGoods(order.goods);
} catch (error) {
console.log("😭😭😭😭", error);
}
}
摆脱一些“如果”
在我们的代码中,我们经常想要提供一些默认值,或者如果出现问题则不调用函数null
。我们可以通过使用布尔检查来避免使用 if 语句。例如,如果我们想根据可能为空或不为空的参数设置默认值,我们可以执行以下操作:const items = newItems || []
。由于 JS 的工作方式,如果 newItems 为空或未定义,则此类布尔检查将返回表达式的值,因此items
将被赋值[]
。这是我从 Addy Osmani 那里学到的一个技巧,尽管我似乎找不到他写的相关文章。有时这会导致代码可读性较差,在这种情况下应该使用 if 语句。
将 API 调用分离到各自的模块中
如果您对共享端点或 API 有多个调用,那么将所有这些 AJAX 调用封装到一个模块中(每个调用都是一个函数)通常会很有帮助。这样,整个应用程序的设置和响应处理就可以保持一致。每当该 API 发生任何更改时,所有更改都可以在一个地方进行,而不会阻塞特定组件的逻辑。
class todoAPI {
getTodos() {
return fetch('/todos').then(response => response.json())
}
editTodo(id) {
return fetch('/todos', {
method: 'POST',
body: JSON.stringify({ id })
}).then(response => response.json())
}
...
}
export const todoAPI = new todoAPI();
稍后当您需要获取待办事项时:
import { todoAPI } from './todoAPI';
const todos = await todoAPI.getTodos();
这使我们能够将 API 传递给任何需要它的模块或组件,而不会将其局限于某个特定的组件。这又是一种提高可重用性并减少重复的方法。
结论
网络不断发展,我的开发实践和理念也同样如此,但上述想法始终萦绕在我的心头。这些想法就像咖啡馆里的常客,即使其他顾客不断变化,他们也会不断光顾。我上面提到的很多想法都来自反复试验。我也阅读了大量关于网络开发的推特文章,并且从不害怕用谷歌搜索来尝试新方法。正如我的好朋友Bennett Dungan所说,学习编程是一场马拉松。建立让你高效的实践和方法需要时间。不妨尝试一下这些方法,然后在推特上告诉我你的想法。
– 猎人✌
原帖发布于hmiller.dev。封面照片由Tim Gouw在Unsplash上拍摄
文章来源:https://dev.to/arthyn/front-end-practices-that-changed-my-life-56md