本文所有內容,純屬個人觀點,無意與任何人爭論
前端技術的現狀
我覺得前端技術發展到現在有兩個最主要的特徵
- 前端工具鏈為前端工程化提供了強有力的支援
這方面主要是
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
就不會影響其他元件內的同名樣式了