【速記】藉助ES6的模版字串,在不用Babel外掛的情況下實現一個輕量級類JSX功能...

weixin_33840661發表於2018-11-26

引發此篇文章是看到了阮一峰的twitter在掘金上的轉載: https://juejin.im/pin/5bf6463...

Preact 的作者建立的利用字串標籤模版實現的類JSX的庫,可以將標籤模版的類JSX字串轉化成類React.createElement或h函式(用來建立 virtual DOM物件的函式): https://github.com/developit/htm

ES6字串介紹(重點閱讀模版字串和標籤模版兩節):http://es6.ruanyifeng.com/#do...

瀏覽器級別API將字串解析成DOM物件:https://developer.mozilla.org...


原始的思考原文如下:

JSX Quasi-Literal

I've been struggling to get the JSX transpiler playing nicely with the traceur compiler, specifically the flags hidden behind --experimental.

The problem is that the since both the JSX transpiler and the traceur compiler are actually parsing the full javascript AST, they would have to mutually agree on the syntax extensions you use: traceur can't parse the faux-xml syntax JSX adds, and JSX can't parse the async or await keywords, for example, or generator functions.

This proof-of-concept is a potential solution: instead of using an external JSX transpiler, we'll parse the faux-xml ourselves, using an ES6 feature called quasi-literals.

Example

define(function(require) {
    var React   = require('react');
    var jsx     = require('lib/jsxquasi');
    var EchoComponent = React.createClass({
        getInitialState: function() {
            return { value: '' };
        },
        handleChange: function() {
            this.setState({ value: this.refs.input.getDOMNode().value });
        },
        render: function() {
            return jsx`
                <div>
                    <input 
                        ref='input' 
                        onChange='${this.handleChange}' 
                        defaultValue='${this.state.value}' />
                    ${this.state.value}
                </div>
            `;
        }
    })
    return function() {
        var comp = jsx`<${EchoComponent} />`;
        React.renderComponent(comp, document.body);
    };
});

A couple of things to notice:

This is valid javascript! Or harmony or es6 or whatever, but importantly, it's not happening outside the js environment. This also allows us to use our standard tooling: the traceur compiler knows how to turn jsx<div>Hello</div>; into the equivalent browser compatible es3, and hence we can use anything the traceur compile accepts!

This is not exactly the same as JSX according to the spec: it includes quotes around the attributes, etc. This is because this parser is based on DOMParser, and hence needs to be valid XML. It would be straighforward though to change it so it matched exactly, or to remove the browser dependency (so it could run on the server, eg.)

index.js


define(function(require) {

    var React = require('react');
    var paramRegex  = /__(\d)+/;
    var parser      = new DOMParser();
    var errorDoc    = parser.parseFromString('INVALID', 'text/xml');
    var errorNs     = errorDoc.getElementsByTagName("parsererror")[0].namespaceURI;
    // turns the array of string parts into a DOM
    // throws if the result is an invalid XML document.
    function quasiToDom(parts) {
    
        // turn ["<div class='", "'>Hi</div>"] 
        // into "<div class='__0'>Hi</div>"
        var xmlstr = parts.reduce((xmlstr, part, i) => {
            xmlstr += part;
            if (i != parts.length - 1) { // the last part has no ending param
                xmlstr += `__${i}`;
            }
            return xmlstr;
        }, "");
       // parse into DOM, check for a parse error
       // browser's DOMParser is neat, but error handling is awful
       var doc      = parser.parseFromString(xmlstr, 'text/xml');
       var errors   = doc.getElementsByTagNameNS(errorNs, 'parsererror');
       var error    = '';
       if (errors.length > 0) {
           error = errors[0].textContent.split('\n')[0];
           throw `invalid jsx: ${error}\n${xmlstr}`; 
       }
       return doc;
    }
    // turn a document into a tree of react components
    // replaces tags, attribute values and text nodes that look like the param
    // placeholder we add above, with the values from the parameters array.
    function domToReact(node, params) {
        var match;
        
        // text node, comment, etc
        if (node.nodeValue) { 
            var value = node.nodeValue.trim();
            if (value.length === 0) {
                return undefined;
            }
            match = value.match(paramRegex);
            return match ? params[parseInt(match[1])] : value;
        }
        // node to get react for
        // if the node name is a placeholder, assume the param is a component class
        var reactNode;
        match = node.localName.match(paramRegex)
        reactNode = match ? params[parseInt(match[1])] : React.DOM[node.localName];
            
        // if we don't have a component, give a better error message
        if (reactNode === undefined) {
            throw `Unknown React component: ${node.localName}, bailing.`;
        }
        // attributes of the node
        var reactAttrs = {};
        for (var i = node.attributes.length - 1; i >= 0; i--) {
            var attr = node.attributes[i];
            reactAttrs[attr.name] = attr.value;
            match = attr.value.match(paramRegex); 
            if (match) {
                reactAttrs[attr.name] = params[parseInt(match[1])];
            }
        }
        // recursively turn children into react components
        var reactChildren = [];
        for (var i = 0; i < node.childNodes.length; i++) {
            var child = node.childNodes[i];
            var reactChild = domToReact(child, params);
            if (reactChild) {
                reactChildren.push(reactChild);
            }
        }
        return reactNode(reactAttrs, reactChildren);
    }
    return function jsx(parts, ...params) {
        var doc     = quasiToDom(parts);
        var react   = domToReact(doc.firstChild, params);
        return react;
    }
});

相關文章