写点什么

手写一个 Vue 风格组件

用户头像
林浩
关注
发布于: 2020 年 07 月 18 日
手写一个Vue风格组件

实现思路:

1.创建一个类 Vue 风格的字符串模板

2.将字符串模板解析成 AST

3.将解析好的 AST 用 JSX 翻译成可供浏览器识别的 DOM 树结构并渲染到浏览器上


前言

自动化构建随着 javascript 的流行逐渐对前端工程师生产率提高愈发重要。现今流行的自动化工具方案不乏选择,有熟知的 Gulp,Grunt,Browserify 和后面会用到的 Webpack,他们都能提供自动化构建,例如 CSS 预处理,图像压缩,代码压缩等功能,前面提到的几个工具都各有优缺点,选择那个工具只需要选择你偏好和其了解程度就行了。


准备工作

配置 webpack, 配合 babel 系列插件,让 es6 语法特性可以自动 polyfil,使其在不同环境不同版本的浏览器平台上正常工作

rules: [            {                test: /\.js$/,                use: {                    loader: "babel-loader",                    options: {                        presets: ['@babel/preset-env'],                        plugins: [["@babel/plugin-transform-react-jsx", {pragma: "createElement"}]]                    }                }            },            {                test: /\.view/,                use: {                    loader: require.resolve("./myloader.js")                }            }        ]
复制代码


  • 配置文件中定义了模块的规则, webpack 再根据文件类型,找到对应的 loader 插件解析, 其中 myloader 是需要我们自定义的一个 loader 插件用于解析字符串串模板

  • 配置文件中使用了 pragma 这个参数,默认的情况下,JSX 语法语法糖会将我们自定义的模板翻译成 React.createElement()

<Cls id="id"> 翻译=> React.createElement(Cls, {id: "id"});
复制代码


编写一个 Loader 插件

module.exports = function(source, map) {	...  return source;}
复制代码

自定义一个 loader 就这样,挺简单的,其中 source 就是 loader 需要处理的源码,也可以借助 node 中 fs 模块自己读取源文件的源码

const fs = require('fs')
const getSources = (filename) => { const code = fs.readFileSync(filename, "utf-8"); return code}console.log(getSources('./carousel.view'))module.exports = getSources;
复制代码

上面的代码将 carousel.vue 中的代码读出来然后 export 到全局,这样就可以在 loader 插件获取到 source 了


接下来要将拿到的字符串解析成 AST,这个过程会用到状态机将 token 进行拆分,我们会以最小意义单元的定义来拆分,例如<html 会被定义成标签的开始 startTag, class="cls", id="id"定义成 attribute, 小于号">"被定义成标签结束 endTag, 文本 text 会被定义成 text, html 中还存在标签自闭合如<img />会被定义成 isSelfClosing,具体实现过程会在后面的文章详细道来

parser.js

let stack = [{type: "document", children:[]}];
function emit(token){ let top = stack[stack.length - 1]; if(token.type == "startTag") { ... } else if(token.type == "endTag") { ... } else if(token.type == "text") { ... }}
const EOF = Symbol("EOF");function data(c){ if(c == "<") { return tagOpen; } else if( c == EOF) { emit({ type:"EOF" }); return ; } else { emit({ type:"text", content:c }); return data; }}
function tagOpen(c){ ...}
function tagName(c) { ...}function beforeAttributeName(c) { ...}function attributeName(c) { ...}function beforeAttributeValue(c) { ...}function doubleQuotedAttributeValue(c) { ...}function singleQuotedAttributeValue(c) { ...}function afterQuotedAttributeValue (c){ ...}function UnquotedAttributeValue(c) { ...}
function selfClosingStartTag(c){ ...}
function endTagOpen(c){ ...} function scriptData(c) { if (c === "<") { return scriptDataLessThanSign; } else { emit({ type: 'text', content: c }) return scriptData; }}function scriptDataLessThanSign(c) { ...}function scriptDataEndTagOpen(c) { ...}function scriptDataEndTagNameS(c) { ...}function scriptDataEndTagNameC(c) { ...}function scriptDataEndTagNameR(c) { ...}function scriptDataEndTagNameI(c) { ...}function scriptDataEndTagNameP(c) { ... }function scriptDataEndTag(c) { ...}function afterAttributeName(c) { ...}
module.exports.parseHTML = function parseHTML(html){ let state = data; for(let c of html) { state = state(c); if (stack[stack.length - 1].tagName === "script" && state == data) { state = scriptData; } } state = state(EOF); return stack[0];}
复制代码

carousel.view

<template>    <div class="wrapper">        <img />    </div></template><script>export default {}</script>
复制代码


最后会得到一串 AST 对象,大概长这个样子

得到 AST 之后,需要将它生成组件的代码,因为 tree 中包含 template 和 script 两个 tagName,可以这么处理

let template = null;let script = null;
for(let node of tree.children) { if (node.tagName == "template") { template = node.children.filter(e => { return e.type !== "text" })[0]; } if (node.tagName == "script") { script = node.children[0].content; }}
复制代码

紧接着下一步需要去创建节点,这里会把状态机里面的关键字过滤掉不需要渲染到 DOM 树上,然后通过递归的方式去 createElement 每个节点的层级

const KeyWords = ["type", "startTag", "endTag", "tagName", "isSelfClosing"];let visit = (node) => {    if (node.type === "text") {       return JSON.stringify(node.content);    }    let attrs = {};    for(let attribute of node.attributes) {       if (KeyWords.includes(attribute.name)) {          continue;       }       attrs[attribute.name] = attribute.value;    }    let children = node.children.map(node => visit(node));    return `createElement("${node.tagName}", ${JSON.stringify(attrs)}, ${children})` }
复制代码


节点创建好了之后,需要将子节点插入到文档树中,在此之前,需要用到前面提到的 JSX 对我们的创建的字符串模板进行翻译成浏览器认识的结构,然后再渲染生成位图展示到浏览器上。

export class Carousel {    render() {        return ${visit(template)};    }    mountTo(parent) {        this.render().mountTo(parent)    }}
复制代码

最后,将生成的 DOM 插入到文档中就实现了从自定义字符串模板创建到浏览器成功渲染的整个过程。

import { Carousel } from "./carousel.view";
let component = new Carousel();component.mountTo(document.body);
复制代码


最终效果:

参考

https://time.geekbang.org/column/article/82397

https://u.geekbang.org/lesson/12?article=215753

https://babeljs.io/docs/en/

https://webpack.js.org/concepts/loaders/

发布于: 2020 年 07 月 18 日阅读数: 90
用户头像

林浩

关注

还未添加个人签名 2019.01.14 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
即将有同学来抄作业了
2020 年 07 月 29 日 15:37
回复
没有更多了
手写一个Vue风格组件