手写一个 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 日 阅读数: 21
用户头像

林浩

关注

还未添加个人签名 2019.01.14 加入

还未添加个人简介

评论

发布
暂无评论
手写一个Vue风格组件