浅谈前端框架
浅谈前端框架
很长一段时间没有写过blog了, 这次更新一篇文章.
这段时间中我接触了各种前端框架, 实现方式等. 前端各种各样的工具, 每一个都让人耳目一新, 却又达不到一个最佳实践的情况.
前端框架层出不穷, 说明了前端人对优化开发的急切需求, 也反映出到现在为止, 没有任何一个框架能使所有人高潮, 没有任何一个框架能做到垄断.
我们前端有和PyPI一样开放的社区, 又有和Go一样强大的核心力量. 开放性使每个人都有能力实现自己优化的想法, 强大的核心又为实现目标提供了超多的可能. 在这些已有的可能中, 又有无数的新的可能在孕育, 等待着唯一的"不可能"的出现.
1. 三剑客
Web Vanilla, 也就是HTML, CSS, JavaScript被称为web三剑客, 这不需要过多介绍.
当我们编写页面时, 原生三剑客遇到的问题有:
- 重复内容难以复用.
- 元素的创建, 删除和状态管理复杂.
- 样式文件无法分级, 结构混乱.
- 过于自由的弱类型语言.
对于最后一项问题, 基本上已经得到了解决, TypeScript语言可以称得上垄断地位, 因此该问题无需多讨论.
对于其他问题, 倘若问我们从后端fetch到一个对象数组数据, 然后加载它们到一个表格中, web原生是这样实现的:
对于遍历内容, 我们要用js一点一点创建元素, 填入属性, 填入class, 填入innerHTML. 简单元素还好, 直接使用模板字符串, 对于复杂或嵌套元素之后用js dom创新, 填入, 追加...
此外, 这样一行如果绑定按钮事件等功能, 还要在渲染之后另写, 还要使用dataset来绑定唯一id, 如有删除/添加事件, 还要重设index, 重设深浅交替的表格形式...
这只是web原生闹心之处的一个小小的地方. 此外, 当我们写好一个好看的按钮, 一个好看的卡片, 一个功能完备的表单, 我们想在其他地方复用它, 对于原生web来说, 只有直接复制粘贴一种办法. 样式还好, 因为.class随时都能使用, 功能呢?
对于一个父类中一堆并列的子类, 如何以整个子类为单位进行编辑呢?
js操作要么笼统到一个容器, 要么精确到每一个最小节点, 别无选择...
当我们写页面时, 会遇到很多层次的分级, 原生CSS提供了BEM命名法, 即Blocker__Element--Modifier, 使用该命名方法可有效进行分级. 然而, CSS是并列的, 每一个写好的{}都是并列的, 没有办法从语法上进行按照HTML的结构进行分级, 这会造成结构非常混乱.
CSS也在所难免的命名公共项目, 如何避免混淆...
Web原生足够强大, 但也足够麻烦.
2. 组件化与复用
组件化是解决上述问题一个非常有效的方式. 在组件中, 我们定义节点, 定义函数, 定义样式. 内部的函数与样式不会与外界的冲突, 组件可以嵌套的调用其他组件, 对于父组件子组件相对独立, 数据可以从父组件向子组件流动...
以上面的表格问题为例, 我们定义行组件, 传入单个对象数据, 渲染成一个独立的<tr>显示在父组件(<tbody>)上, 父组件只要遍历数组并将对象传递给子组件即可. 对于添加和删除, 无非就是创建新的子组件与移除已有的子组件; 编辑只不过是更新某个子组件的数据. 我们再用数据中的id作为子组件的唯一标识符, 父组件更新index, 不需要额外的干扰.
React实现如下:
interface Data {
id: number;
...
}
function Tr(props: {index: number} & Data) {
return (
<tr>
<td>{{ props.id }}</td>
...
</tr>
);
}
function Tbody(props: Data[]) {
return (
<tbody>
{props.map((item, index) => (
<Tr key={data.id} {...item} index={index} />
))}
</tbody>
);
}至于子项的添加编辑与删除, 子项的唯一性由React接管, 无需自行复杂控制.
Vue实现同理.
3. 声明式与函数式
声明式编程告诉程序是什么, 函数式编程告诉程序怎么做.
原生Web是声明式编程, 尤其是CSS, 十分典型的声明式编程.
Vue相对折中, React是极端的函数式编程(这也注定React的CSS写法很烂).
对于一个HTML页面, 我们即希望它一口气加载一堆元素, 又希望我们能动态的加载一些节点, 前者适合直接声明, 后者适合用函数一点一点写出.
对于CSS样式(CSS, SCSS, SASS等同理), 它们都是一口气写完一整个组, 至于浏览器怎么加载, 按什么顺序加载, 加载到前面的空值但后面有写怎么办, 这些对于声明式编程无所谓.
.container {
padding: 1rem;
border-radius: 8px;
background-color: #ffffff22;
font-size: xx-large;
}container.style.padding = "1rem";
container.style.borderRadius = "8px";
container.style.backgroundColor = "#ffffff22";
container.style.fontSize = "xx-large";声明式写起来更方便, 函数式更适合动态修改.
Vue可以优雅的使用CSS, 而React强制要求使用后者(或React函数化的style对象).
所幸的是React有styled-components包, 可以在字符串的层面转换它们, 通过模板字符串内嵌js语句, 这样就兼具了二者的优点.
const Button = styled.button`
width: 120px;
height: 40px;
border: 2px solid ${props => props.color};
color: ${props => props.color};
`;4. 框架, 组件库与原子化css
Vue和React的区别上面已经写的差不多, 这里不再讨论这些.
Vue和React分别有nuxt和next这样的一键构建工具, 极大的方便了使用.
其中出现了React + 原子化CSS(尤其是Tailwind CSS)的搭配.
原子化CSS在快速编写样式中极为方便, 对于小项目, 无复用需求的情况优点非常明显.
例如, 我们写一个样式漂亮的按钮, 这一般需要非常多的css属性, 此外一般还要添加后伪元素, 伪类, 动画. 这用CSS写起来十分困难. 但如果使用tailwind, 它提供了丰富的简写方法和预设, 能非常轻松.
但我们想要复用它...
.button {...}
.button:hover {...}
.button:active {...}
.button:focus {...}
.button::before {...}直接class="button"即可, 但如果是tailwind...
<button
id="..."
...
class="... ... ... ..."
>
...
</button>复用方法只能复制粘贴, 虽然有tailwind组件可以使用:
@layer components {
.button {
@apply ... ... ... ...;
}
}然而@apply强制写到一行, 不能换行. 因为tailwind官方推荐把整个按钮抽象为组件, 而非只抽象样式, 所以使用这种方法强迫开发者. 那么对于只是想要复用样式, 不想干扰太多东西时候, 无法抽象组件, 而使用样式又要么写复杂的css, 要么用官方主动恶心人的tailwind.
这不是tailwind的问题, 但tailwind强制不想解决.
这种原子化和组件复用是相背的, 同样和函数式编程也是相背的.
但React中没有方便的直接写css的方案, 只能使用tailwind. 对于动态的tailwind标签还需要手动预加载.
例如:
className=`p-${padding} w-full h-full ...`
// 不可用! 因为"p-${padding}"不是有效的tailwind关键词, tailwind编译中模板加载阶段, 而该属性生效(被拼完成)在js加载阶段, 因此产生的关键词可能根本没有被编译.
const paddingList = {
1: "p-1",
2: "p-2",
...
};
className=`${paddingList[padding]} w-full h-full ...`
// 可用. 因为可能出现的结果已经编译好了.总而言之. 简单项目(少复用), tailwind远优于css, 直接在html中使用绝对是好选择; 复杂项目(少复用)也可以使用tailwind, 但一旦有复用需求, 强自定义需求, 还是用css吧.
如果你的项目直接使用了现成的组件库, 那么有两种可行的方案: 要么一行css都不要写, 使用组件库提供的组件样式和class;; 要么写原生css. 这种情况原子化css十分不合适, 除非你的组件库官方就写明了支持某某原子化css.
你的原子化css的什么类名可能与组件库的冲突, 而且tailwind和组件库css是分开编译的, 最终谁后编译谁应用优先, 不一定出现什么不可预知的问题.