浅谈前端框架
浅谈前端框架
很长一段时间没有写过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是分开编译的, 最终谁后编译谁应用优先, 不一定出现什么不可预知的问题.