挑战21天手写前端框架 day3 让页面运行起来

1,505 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

阅读本文需要 3 分钟,编写本文耗时 3 小时,因为不知道该从哪个角度切入会更加合适,所以涂涂改改。纠结半天,最终确定下来今天的内容。

之前看 phodal 的低代码相关的文章,里面有一句话大概说的是,项目的复杂度就像力一样,只会发生转移而不会凭空消失。

我觉得这句话在好多地方都是适用的,比如我们要手写一个前端框架的目的就是将手写原生web项目的复杂度转移到框架中。

所以要手写前端框架,我们可以回归到前端开发的最初的起点,回想一下你学习前端开发的第一个 demo。

比如我们写一个简单的 Hello World:

<!DOCTYPE html>
<html lang="en">
<body>
    <div id="malita">
        <span>loading...</span>
    </div>
    <script>
        const root = document.getElementById('malita');
        root.innerHTML = '<span>Hello Malita!</span>';
        root.addEventListener('click', (event) => {
            event.target.innerHTML = '<span>Hi!</span>';
        })
    </script>
</body>
</html>

有点前端基础的同学应该一眼就能看出上面的代码逻辑,就是在页面上显示 ‘Hello Malita!’,然后用户点击之后变成了 ‘Hi!’。

上面的需求虽然非常的简单,甚至没有前端基础的同学多看几眼也能看懂,但是如果使用现代前端框架中常常被提及的 MVC 架构去分析的话。

Hello Malita!Hi! 是数据,存放在 Model 层; <span> 是视图存放在 View 层; 点击事件和数值变化是控制层存放在 Controller

将上面的代码拆解之后,得到以下的代码段,夸张点说,这就是 React 和 Vue 的核心源码,这可不是不买会员就可以观看的内容哦。 (我吹的,我不会Vue)

<script>
        function Model() {
            this.text = 'Hello Malita!';
            this.setText = text => this.text = text;
        }

        function View(controller) {
            const self = this;
            self.root = document.getElementById('malita');
            self.root.addEventListener('click', controller.onClick)
            this.render = () => {
                const text = controller.getModel().text;
                self.root.innerHTML = `<span>${text}</span>`;
            }
        }

        function Controller(model) {
            const self = this;
            self.model = model;
            this.onClick = (event) => {
                self.model.setText('Hi!');
                // 数据驱动页面,页面应该根据 model 更改响应 render
                const text = controller.getModel().text;
                event.target.innerHTML = `<span>${text}</span>`;
            }
            this.getModel = () => {
                return self.model;
            }
        }
        const model = new Model();
        const controller = new Controller(model);
        const view = new View(controller);
        view.render();
    </script>

递进一步的我们来看下使用 React 如何实现上面的功能。

为了便于后续的开发,(其实是我懒得复制 cdn 链接)我们在当前项目的根目录下,安装 react 和 react-dom

npm i react react-dom

然后在 demo 中引入

    <script src="../../node_modules/react/umd/react.development.js"></script>
    <script src="../../node_modules/react-dom/umd/react-dom.development.js"></script>

然后使用 React 的原始用法来实现上述的功能

<body>
    <div id="malita">
        <span>loading...</span>
    </div>
    <script>
        const Hello = () => {
            const [text, setText] = React.useState('Hello Malita!');
            return React.createElement("span", {
                onClick: () => {
                    setText('Hi!');
                }
            }, text);
        };

        const root = ReactDOM.createRoot(document.getElementById('malita'));
        root.render(React.createElement(Hello));
    </script>
</body>

如果你没有 React 基础,那这个用例和上面的哪些用例可以比对着,仔细看看。

使用 createElement 的语法进行 React 开发,最大的问题就是当有层级嵌套时,会变得非常的复杂。可读性极差。

比如简单的

<div><p>Hello<span>Malita!</span></p></div>

需要编写以下代码:

React.createElement("div", null, React.createElement("p", null, "Hello", React.createElement("span", null, "Malita!")));

就相当于你在写 html 的时候,都不使用 html 的标签,而是全部使用 js 方法 document.createElement(tagName) 来编写整个页面。

所以为了更加便于书写和阅读,充分利用 html 和 js 的优势,我们进行 React 开发一般都会使用 jsx 语法。

依旧是上面的需求,我们引入 jsx,代码变得更直观了:

<body>
    <div id="malita">
        <span>loading...</span>
    </div>
    <script>
        const Hello = () => { 
            const [text, setText] = React.useState('Hello Malita!'); 
            return (<span onClick={()=> {
                setText('Hi!')
            }}> {text} </span>);
        }; 
        const root = ReactDOM.createRoot(document.getElementById('malita')); 
        root.render(React.createElement(Hello));
    </script>
</body>

但是写完发现,浏览器是无法识别 jsx 语法的,所以我们要引入 babel 来将我们的 jsx 语法转换成 React 的原始语法。

其实 babel 的实现原理非常简单,就是通过将代码转换成抽象语法书 (AST),再转换成目标语法,记下来,这是面试题。

在项目中安装 babel-standalone

npm i babel-standalone

然后在项目中引入,并且修改 script 的 type 为 text/babel

<head>
+ <script src="../../node_modules//babel-standalone/babel.min.js"></script>
</head>

<body>
    <div id="malita">
        <span>loading...</span>
    </div>
-    <script>
+    <script type="text/babel">
    </script>
</body>

我测试的时候使用的是谷歌浏览器 V100 ,所以里面用到的 ES6 的一些语法,它都可以识别,后续我们也会使用 babel 将我们的 es6 代码转换成 es5。

到此我们就完成了让页面运行起来的所有尝试。

值得注意的是,这不是一个 React 的零基础入门教程,文章的用意在于讲清楚前端框架的部分运行原理和工作内容,你可以把这当作一篇需求文档来对待,因为我们会在后续的内容中,用更加科学合理的方式来实现它。

在这一个系列中,我会尽量讲明“为什么使用XXX”,它解了什么问题。

感谢阅读,如果你有其他的疑问,或者对这个系列书写的角度和切入点有其他的看法和建议的话,欢迎在评论区和我互动。

源码归档