经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » CSS » 查看文章
脚踏esbuild祥云,胸怀tsx利刃,身披scss羽衣,追寻前端的本质
来源:cnblogs  作者:liulun  时间:2023/11/17 9:20:41  对本文有异议

本文所有内容,纯属个人观点,无意与任何人争论

前端技术的现状

我觉得前端技术发展到现在有两个最主要的特征

  1. 前端工具链为前端工程化提供了强有力的支持

这方面主要是webpackrollupesbuild等工具产生的价值,当然还有背后的Node.js

这些工具让前端开发者可以更从容的开发大型前端项目。

  1. 前端开发框架提升了前端工程师的生产效率

这方面主要是AngularReactVueSvelte等开发框架产生的价值。

这些框架让开发者可以更容易的开发前端项目

前端工具链的价值毋庸置疑,但前端开发框架的价值与影响值得讨论。

前端开发框架之所以能提升前端工程师的生产效率,是因为它为我们做了大量的封装。

这种封装工作在提升生产效率的同时也带来了复杂性,甚至有些封装工作的复杂程度远超了业务逻辑本身。

比如:我们修改一个变量的值,并把这个值更新到Dom中,

在不使用前端框架时,我们一般会写这样的代码

  1. let count = 0
  2. count + = 1;
  3. let dom = document.getElementById("id")
  4. dom.innerHTML = count

使用前端框架后,写的代码变成了这样:

  1. // Vue
  2. // <div>{{count}}</div>
  3. let count = ref(0)
  4. count.value += 1
  1. // React
  2. // <div>{count}</div>
  3. const [count, setCount] = useState(0);
  4. setCount(count + 1);
  1. // Svelte
  2. // <div>{count}</div>
  3. let count = 0;
  4. count += 1;

如你所见,前端开发框架帮开发者做了大量的工作,比如:虚拟DOM,Diff算法,代理观察变化等等。

大有 为了一碟醋,包了一锅饺子 的嫌疑,就算这锅饺子是尤雨溪帮我们包的,

我们也很难说饺子馅里油多了还是油少了,饺子皮是高筋面粉还是低筋面粉。

甚至现在大家都不考虑自己的身子适不适合吃饺子了,既然是尤雨溪帮我包的,那我一定要吃呀!

当我们的页面变卡、页面占用的内存逐渐上升最后OOM时,

我们有考虑过,如果不用这些框架,是不是这类问题更容易被发现,更容易被控制呢?

(当然,这里提到的问题,一定是我们吃饺子的姿势不对导致的,不是饺子本身的问题_

回归前端的本质

我们要回到前端开发者刀耕火种、茹毛饮血的时代吗?当然不是。

那么哪些东西是我们不想放弃的?

  1. 组件化开发的模式

标题栏一个组件,侧边栏一个组件,菜单一个组件,各个组件有各个组件各自的业务逻辑。

  1. 困扎代码

发布之前,各个组件的代码会被困扎到一起,产出很多个chunk文件,tree-shake会帮我们移除没用到的代码

  1. 热更新或热重载的能力

改了某个组件的代码,能实时看到改动后的结果,如果达不到热更新,那就保留最基本的热重载能力。

  1. 样式隔离

不一定要Shadow Dom,我们可以制定一套规则来约束组件的样式。

  1. 强类型与智能提示

最好有TypeScript的强类型支持,写组件的时候最好能有足够多的智能提示

除了这些东西之外,

像虚拟Dom,Diff算法,Watch对象的变化,组件间通信,数据绑定等,

我们都可以抛弃,这些本来就是我们自己的工作,不需要框架来帮我们做。

归根结底:在写代码的时候,我们要始终知道自己在做什么

方案

  1. 基于 Web Component 技术与相关的辅助工具

单纯用 Web Component 开发的话,挺麻烦的。

要写一个工具才才能提升我们使用这个方案的开发体验,

比如把templatecss样式代码文件封装到一个单独的组件中

搞定这个工具没那么容易,而且搞不好又回到了老路上,等于自己开发了一个前端框架,

我在这个方向上做过一些尝试,后来就放弃了

  1. 基于 JSX/TSX 技术及相关辅助工具

现在VSCodeJSX/TSX语法支持的很好,esbuild也内置支持对JSX/TSX的困扎

最关键的是:实现一个简单的JSX/TSX解析器非常容易(不依赖React库)

JSX/TSX解析器

废话不多说,直接看解析器的代码吧:

  1. // React.ts
  2. let appendChild = (children: any,node: Node)=> {
  3. if (Array.isArray(children)) {
  4. for (const child of children) {
  5. if(child) appendChild(child,node)
  6. }
  7. } else if (typeof children === "string" || typeof children === "number") {
  8. let textNode = document.createTextNode(children as any)
  9. node.appendChild(textNode)
  10. } else if (typeof children.nodeType === "number") {
  11. node.appendChild(children)
  12. }
  13. }
  14. let appendAttr = (attr: object,node: HTMLElement)=>{
  15. for (let key of Object.keys(attr)) {
  16. let val = attr[key];
  17. if(key === "style"){
  18. node.setAttribute("style", val)
  19. } else if(typeof val === "function"){
  20. if(key.startsWith("on")){
  21. node.addEventListener(key.toLocaleLowerCase().substring(2), val)
  22. }
  23. } else if(typeof val === "object"){
  24. node[key] = val
  25. }
  26. else {
  27. node.setAttribute(key, val)
  28. }
  29. }
  30. }
  31. let createElement = (tag: any, attr: any, ...children: any[]) => {
  32. if(typeof tag === "string"){
  33. let node = document.createElement(tag);
  34. if(attr) appendAttr(attr,node)
  35. if(children) appendChild(children,node)
  36. return node;
  37. } else if(typeof tag === "function"){
  38. let obj = tag({...attr,children})
  39. return obj
  40. }
  41. }
  42. let Fragment = (attr:any) =>{
  43. const fragment = document.createDocumentFragment()
  44. appendChild(attr.children, fragment)
  45. return fragment
  46. }
  47. export default {
  48. createElement,
  49. Fragment
  50. }

没错,就这么4个简单的方法,就能解析大部分JSX/TSX语法

像在JSX/TSX中使用SVG这类需求,我就直接忽略了,遇到这类需求用原始的HTML方法处理最好

下面是一个简单的示例

  1. import React from "./React";
  2. let App = ()=>{
  3. let count = 1;
  4. return <div>{count}</div>
  5. }
  6. document.body.appendChild(<App/>);

这个组件的第一行导入了前面介绍的四个方法

注意:这个组件中没有使用任何React对象的方法,也得导入React对象,而且必须叫React对象,不然esbuild不认。

子组件示例

  1. //主组件 App.tsx
  2. import React from "./React";
  3. import LeftPanel from "./LeftPanel";
  4. import MainPanel from "./MainPanel";
  5. let App = ()=>{
  6. return <><LeftPanel/><MainPanel/></>
  7. }
  8. document.body.appendChild(<App/>);
  1. // 子组件 LeftPanel.tsx
  2. import React from "./React";
  3. export default function () {
  4. let count = 1;
  5. return <div>{count}</div>
  6. }

其他一些动态创建元素的方法也都支持,比如:

  1. //示例1
  2. <div>
  3. {[...Array(8)].map((v,i)=><div>{`${i}`}</div>) }
  4. </div>
  1. //示例2
  2. let container = document.getElementById("container");
  3. for(let i=0;i<6;i++){
  4. let row = <div class="row"></div>
  5. for(let j=0;j<7;j++){
  6. let cell = <div><div class="cellHeader">{obj.content}</div></div>
  7. row.appendChild(cell)
  8. }
  9. container.append(row)
  10. }

用esbuild启动调试服务器

先来看脚本代码:

  1. // ./script/dev.js
  2. let esbuild = require("esbuild")
  3. let {sassPlugin} = require("esbuild-sass-plugin")
  4. let fs = require("fs")
  5. let startDevServer = async ()=>{
  6. let content = `<html><head>
  7. <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  8. <link rel="stylesheet" href="./Index.css">
  9. </head><body>
  10. <script src="./Index.js"></script>
  11. <script>
  12. new EventSource('/esbuild').addEventListener('change', () => location.reload())
  13. </script>
  14. </body></html>`;
  15. await fs.writeFile(`./dist/Index.html`,content)
  16. let ctx = await esbuild.context({
  17. entryPoints: [`./Index.tsx`],
  18. bundle: true,
  19. outdir: 'dist',
  20. plugins: [sassPlugin()],
  21. sourcemap:true
  22. })
  23. await ctx.watch()
  24. let { host, port } = await ctx.serve({
  25. servedir: 'dist',
  26. })
  27. let devServerAddr = `http://localhost:${port}/index.html`
  28. console.log(devServerAddr)
  29. }
  30. startDevServer();

有了这个脚本之后,你只要在package.json中加一行这样的指令

  1. "dev": "node ./script/dev.js",

就可以通过这个命令行命令

  1. npm run dev

启动你得调试页面了

如你所见,我们为esbuild增加了esbuild-sass-plugin插件,这样我们就可以在tsx/jsx组件中使用scss样式了

  1. import "./Index.scss";

上面的模板html代码中有一行这样得脚本

  1. new EventSource('/esbuild').addEventListener('change', () => location.reload())

此脚本为esbuild的热重载服务,

当我们修改某个组件的代码时,整个页面会跟着刷新

这不是热更新,只是热重载,有它就够了,上热更新代价太大,就不要自行车了。

esbuild 打包产物

先看代码

  1. // ./script/release.js
  2. let esbuild = require("esbuild")
  3. let {sassPlugin} = require("esbuild-sass-plugin")
  4. let fs = require("fs")
  5. let release = async ()=>{
  6. let content = `<html><head>
  7. <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  8. <link rel="stylesheet" href="./Index.css">
  9. </head><body><script src="./Index.js"></script></body></html>`;
  10. await fs.writeFile(`./release/Index.html`,content)
  11. let ctx = await esbuild.build({
  12. entryPoints: [`./Index.tsx`],
  13. bundle: true,
  14. outdir: 'release',
  15. plugins: [sassPlugin()],
  16. minify: true,
  17. sourcemap:false
  18. })
  19. console.log("build ok")
  20. }
  21. release();

package.json中加入:

  1. "release": "node ./script/release.js"

打包指令:

  1. npm run release

打包代码比较简单,关键点是minify设置为true以压缩输出产物。

scss 隔离样式

假设我们约定一个组件的根元素有一个父样式,

这个父样式约束着这个组件的所有子元素得样式

那就可以用下面的代码,让组件的样式作用于组件内,不污染全局样式

  1. //ViewDay.scss
  2. #ViewDay{
  3. cursor: pointer;
  4. .bgLine{
  5. //
  6. }
  7. #JobContainer{
  8. //
  9. }
  10. }
  1. // 子组件 ViewDay.tsx
  2. import React from "./React";
  3. import "./ViewDay.scss";
  4. export default function () {
  5. return <div id="ViewDay">
  6. <div class="bgLine"></div>
  7. <div id="JobContainer"></div>
  8. </div>
  9. }

这样 .bgLine#JobContainer 就不会影响其他组件内的同名样式了

原文链接:https://www.cnblogs.com/liulun/p/17833480.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号