脚踏esbuild祥云,胸怀tsx利刃,身披scss羽衣,追寻前端的本质
本文所有內容,純屬個人觀點,無意與任何人爭論
前端技術的現狀
我覺得前端技術發展到現在有兩個最主要的特征
- 前端工具鏈為前端工程化提供了強有力的支持
這方面主要是
webpack、rollup、esbuild等工具產生的價值,當然還有背后的Node.js。這些工具讓前端開發者可以更從容的開發大型前端項目。
- 前端開發框架提升了前端工程師的生產效率
這方面主要是
Angular、React、Vue和Svelte等開發框架產生的價值。這些框架讓開發者可以更容易的開發前端項目
前端工具鏈的價值毋庸置疑,但前端開發框架的價值與影響值得討論。
前端開發框架之所以能提升前端工程師的生產效率,是因為它為我們做了大量的封裝。
這種封裝工作在提升生產效率的同時也帶來了復雜性,甚至有些封裝工作的復雜程度遠超了業務邏輯本身。
比如:我們修改一個變量的值,并把這個值更新到Dom中,
在不使用前端框架時,我們一般會寫這樣的代碼
let count = 0
count + = 1;
let dom = document.getElementById("id")
dom.innerHTML = count
使用前端框架后,寫的代碼變成了這樣:
// Vue
// <div>{{count}}</div>
let count = ref(0)
count.value += 1
// React
// <div>{count}</div>
const [count, setCount] = useState(0);
setCount(count + 1);
// Svelte
// <div>{count}</div>
let count = 0;
count += 1;
如你所見,前端開發框架幫開發者做了大量的工作,比如:虛擬DOM,Diff算法,代理觀察變化等等。
大有 為了一碟醋,包了一鍋餃子 的嫌疑,就算這鍋餃子是尤雨溪幫我們包的,
我們也很難說餃子餡里油多了還是油少了,餃子皮是高筋面粉還是低筋面粉。
甚至現在大家都不考慮自己的身子適不適合吃餃子了,既然是尤雨溪幫我包的,那我一定要吃呀!
當我們的頁面變卡、頁面占用的內存逐漸上升最后OOM時,
我們有考慮過,如果不用這些框架,是不是這類問題更容易被發現,更容易被控制呢?
(當然,這里提到的問題,一定是我們吃餃子的姿勢不對導致的,不是餃子本身的問題_)
回歸前端的本質
我們要回到前端開發者刀耕火種、茹毛飲血的時代嗎?當然不是。
那么哪些東西是我們不想放棄的?
- 組件化開發的模式
標題欄一個組件,側邊欄一個組件,菜單一個組件,各個組件有各個組件各自的業務邏輯。
- 困扎代碼
發布之前,各個組件的代碼會被困扎到一起,產出很多個chunk文件,
tree-shake會幫我們移除沒用到的代碼
- 熱更新或熱重載的能力
改了某個組件的代碼,能實時看到改動后的結果,如果達不到熱更新,那就保留最基本的熱重載能力。
- 樣式隔離
不一定要Shadow Dom,我們可以制定一套規則來約束組件的樣式。
- 強類型與智能提示
最好有
TypeScript的強類型支持,寫組件的時候最好能有足夠多的智能提示
除了這些東西之外,
像虛擬Dom,Diff算法,Watch對象的變化,組件間通信,數據綁定等,
我們都可以拋棄,這些本來就是我們自己的工作,不需要框架來幫我們做。
歸根結底:在寫代碼的時候,我們要始終知道自己在做什么。
方案
- 基于
Web Component技術與相關的輔助工具
單純用 Web Component 開發的話,挺麻煩的。
要寫一個工具才才能提升我們使用這個方案的開發體驗,
比如把
template、css樣式和代碼文件封裝到一個單獨的組件中搞定這個工具沒那么容易,而且搞不好又回到了老路上,等于自己開發了一個前端框架,
我在這個方向上做過一些嘗試,后來就放棄了
- 基于
JSX/TSX技術及相關輔助工具
現在
VSCode對JSX/TSX語法支持的很好,esbuild也內置支持對JSX/TSX的困扎最關鍵的是:實現一個簡單的
JSX/TSX解析器非常容易(不依賴React庫)
JSX/TSX解析器
廢話不多說,直接看解析器的代碼吧:
// React.ts
let appendChild = (children: any,node: Node)=> {
if (Array.isArray(children)) {
for (const child of children) {
if(child) appendChild(child,node)
}
} else if (typeof children === "string" || typeof children === "number") {
let textNode = document.createTextNode(children as any)
node.appendChild(textNode)
} else if (typeof children.nodeType === "number") {
node.appendChild(children)
}
}
let appendAttr = (attr: object,node: HTMLElement)=>{
for (let key of Object.keys(attr)) {
let val = attr[key];
if(key === "style"){
node.setAttribute("style", val)
} else if(typeof val === "function"){
if(key.startsWith("on")){
node.addEventListener(key.toLocaleLowerCase().substring(2), val)
}
} else if(typeof val === "object"){
node[key] = val
}
else {
node.setAttribute(key, val)
}
}
}
let createElement = (tag: any, attr: any, ...children: any[]) => {
if(typeof tag === "string"){
let node = document.createElement(tag);
if(attr) appendAttr(attr,node)
if(children) appendChild(children,node)
return node;
} else if(typeof tag === "function"){
let obj = tag({...attr,children})
return obj
}
}
let Fragment = (attr:any) =>{
const fragment = document.createDocumentFragment()
appendChild(attr.children, fragment)
return fragment
}
export default {
createElement,
Fragment
}
沒錯,就這么4個簡單的方法,就能解析大部分JSX/TSX語法
像在
JSX/TSX中使用SVG這類需求,我就直接忽略了,遇到這類需求用原始的HTML方法處理最好
下面是一個簡單的示例
import React from "./React";
let App = ()=>{
let count = 1;
return <div>{count}</div>
}
document.body.appendChild(<App/>);
這個組件的第一行導入了前面介紹的四個方法
注意:這個組件中沒有使用任何React對象的方法,也得導入React對象,而且必須叫React對象,不然esbuild不認。
子組件示例
//主組件 App.tsx
import React from "./React";
import LeftPanel from "./LeftPanel";
import MainPanel from "./MainPanel";
let App = ()=>{
return <><LeftPanel/><MainPanel/></>
}
document.body.appendChild(<App/>);
// 子組件 LeftPanel.tsx
import React from "./React";
export default function () {
let count = 1;
return <div>{count}</div>
}
其他一些動態創建元素的方法也都支持,比如:
//示例1
<div>
{[...Array(8)].map((v,i)=><div>{`${i}`}</div>) }
</div>
//示例2
let container = document.getElementById("container");
for(let i=0;i<6;i++){
let row = <div class="row"></div>
for(let j=0;j<7;j++){
let cell = <div><div class="cellHeader">{obj.content}</div></div>
row.appendChild(cell)
}
container.append(row)
}
用esbuild啟動調試服務器
先來看腳本代碼:
// ./script/dev.js
let esbuild = require("esbuild")
let {sassPlugin} = require("esbuild-sass-plugin")
let fs = require("fs")
let startDevServer = async ()=>{
let content = `<html><head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="stylesheet" href="./Index.css">
</head><body>
<script src="./Index.js"></script>
<script>
new EventSource('/esbuild').addEventListener('change', () => location.reload())
</script>
</body></html>`;
await fs.writeFile(`./dist/Index.html`,content)
let ctx = await esbuild.context({
entryPoints: [`./Index.tsx`],
bundle: true,
outdir: 'dist',
plugins: [sassPlugin()],
sourcemap:true
})
await ctx.watch()
let { host, port } = await ctx.serve({
servedir: 'dist',
})
let devServerAddr = `http://localhost:${port}/index.html`
console.log(devServerAddr)
}
startDevServer();
有了這個腳本之后,你只要在package.json中加一行這樣的指令
"dev": "node ./script/dev.js",
就可以通過這個命令行命令
npm run dev
啟動你得調試頁面了
如你所見,我們為esbuild增加了esbuild-sass-plugin插件,這樣我們就可以在tsx/jsx組件中使用scss樣式了
import "./Index.scss";
上面的模板html代碼中有一行這樣得腳本
new EventSource('/esbuild').addEventListener('change', () => location.reload())
此腳本為esbuild的熱重載服務,
當我們修改某個組件的代碼時,整個頁面會跟著刷新
這不是熱更新,只是熱重載,有它就夠了,上熱更新代價太大,就不要自行車了。
esbuild 打包產物
先看代碼
// ./script/release.js
let esbuild = require("esbuild")
let {sassPlugin} = require("esbuild-sass-plugin")
let fs = require("fs")
let release = async ()=>{
let content = `<html><head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="stylesheet" href="./Index.css">
</head><body><script src="./Index.js"></script></body></html>`;
await fs.writeFile(`./release/Index.html`,content)
let ctx = await esbuild.build({
entryPoints: [`./Index.tsx`],
bundle: true,
outdir: 'release',
plugins: [sassPlugin()],
minify: true,
sourcemap:false
})
console.log("build ok")
}
release();
package.json中加入:
"release": "node ./script/release.js"
打包指令:
npm run release
打包代碼比較簡單,關鍵點是minify設置為true以壓縮輸出產物。
scss 隔離樣式
假設我們約定一個組件的根元素有一個父樣式,
這個父樣式約束著這個組件的所有子元素得樣式
那就可以用下面的代碼,讓組件的樣式作用于組件內,不污染全局樣式
//ViewDay.scss
#ViewDay{
cursor: pointer;
.bgLine{
//
}
#JobContainer{
//
}
}
// 子組件 ViewDay.tsx
import React from "./React";
import "./ViewDay.scss";
export default function () {
return <div id="ViewDay">
<div class="bgLine"></div>
<div id="JobContainer"></div>
</div>
}
這樣 .bgLine 和 #JobContainer 就不會影響其他組件內的同名樣式了
總結
以上是生活随笔為你收集整理的脚踏esbuild祥云,胸怀tsx利刃,身披scss羽衣,追寻前端的本质的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Lambda 表达式常见面试问
- 下一篇: 神经网络入门篇:激活函数(Activat