jsx已经是一个公认的新流行模版语言,特别是在现代前端编程潮流下,打破传统html、js、style分家的模式,将全部内容模块化,用js黏合在一起,react就是集大成者,将UI操作的底层逻辑和宿主环境(浏览器)的底层实现分离,实现了一整套的前端解决方案。抛开各种坑,你不得不承认jsx是所有前端模板语言里面最好用的,因为它就是js本身,只是用了一种新的语法糖代替。
重新认识JSX
注意我这里使用了大写的JSX。虽然react在官方文档中非常清楚的介绍了jsx,但是对于开发者而言,这份文档仅框定在react框架下,你无法脱离react。现在,我们脱离react来重新认识jsx。
模板语言
首先把它当作一个模板语言,但是请记住,在现代前端编程中,任何项目都是模块化的,因此,我们想要使用jsx作为模板引擎的时候,不能像angular或handlebars那样,创建一个完全基于html合法语法的文件,而是应该创建一个js文件,为了和单纯的js文件区分,使用.jsx作为文件后缀名,但它本质上是一个js文件:
// tpl.js module.exports = ( <div> <img src="..." /> </div> );
这样就是一个.jsx文件,但是这样有一个不好的地方,你无法像handlebars一样从外面传变量进来,所以,我们进行一系列的改造:
// tpl.js module.exports = function() { return ( <div> <img src="..." /> </div> ); };
返回一个函数,这样,在外部可以通过引入这个函数,传入参数的形式来给模板传入变量,甚至是函数。由于jsx本身是js,所以在模板中绑定事件变得非常容易。
模板解析
既然是模板,那么必然有一个类似parser之类的东西来对模板进行解析,让开发者可以将模板加载到自己的DOM结构中。但是先不要着急,对于一个新的语法,它还需要一个语法支持的引擎。非常幸运,我们有了babel,这是handlebars那个年代没有的。通过babel的插件,可以很容易对某些新语法进行支持,甚至自己发明一个新语法。
virtual dom流行之后,我们可以非常容易想到,任何的模板引擎,解析的结果,都可以是virtual dom,拿到virtual dom之后,我们可以干的事情实在太多了,问题在于,我们需要一个高效的解析器,我们需要一个可以在babel插件使jsx语法被支持的情况下,把jsx模板解析为virtual dom并且同时保留jsx所有特性的解析器。非常幸运,前人已经踩完坑,帮我们把路铺好了。
创建实例
在得到virtual dom之后,我们顺理成章的事是将virtual patch到真实的DOM结构中。之所以用patch这个词,是因为我脑海中还保留着diff操作,如若不考虑这一个问题,这里的问题,就是如何将模板解析得到的一个“将html结构抽象为js对象”的对象再重新翻译为DOM实例,并插入到具体的节点中去。
就这样一个过程,手写jsx代码,借助babel进行解析,再通过另外某个工具(比如react)创建实例,使得jsx可以在任何项目中担任模板引擎的角色。而接下来,我就简单把这些卖的关子统统抖出来。
实例操作
首先,我们要手写一个.jsx文件,把它放在某个目录中,这个.jsx里面用JSX语法写模板,用module.exports = function() {}的方式导出模块。
// tpl.jsx module.exports = function(items) { return ( <div className="items"> <ol> {items.map(item => <li>{item.name}: {item.value}</li>)} </ol> </div> ) }
现在你有一个只有一个.jsx文件的目录,我们要在这个目录里面创建一个npm项目,这样才能更方便的使用babel进行编译。
$ npm init $ npm install babel-cli babel-core babel-plugin-transform-jsx babel-plugin-syntax-jsx
并且,我们创建一个.babelrc文件,内容为:
{ "plugins": ["syntax-jsx", "transform-jsx"] }
你已经看到了,我用了两个babel插件,一个是babel-plugin-syntax-jsx,它是用来让babel识别JSX语法的,另一个是babel-plugin-transform-jsx,用来将jsx转换为virtual dom。
我们要在package.json文件的script中增加一条命令用来编译:
"scripts": { "compile": "babel tpl.jsx -o tpl_out.js" },
这时我们在命令行中运行一下:
$ npm run compile
然后发现目录下多了一个tpl_out.js文件,打开看看是不是有惊喜。
我们得到的tpl_out.js文件里面,你会发现,你写的jsx全部被编译为一个object了
看着这样的结构,我们可以非常清晰且知道每一个属性代表着什么意思。它代表着一个html片段的抽象表达。现在,我们要想办法把这个抽象表达重新转换为真实的DOM,并插入到文档中。实际上,我们只需要写一个函数,将这个virtual dom生成DOM就行了。但是已经有大神帮我们写了现成的代码,你可以阅读这里获得createElement函数。这里把源代码抄过来,防止哪一天这个页面被删除。
/** @jsx createElement */ const HTML_TAGS = { a: { name: 'a', attributes: { download: 'download', href: 'href', hrefLang: 'hreflang', ping: 'ping', referrerPolicy: 'referrerpolicy', rel: 'rel', target: 'target', type: 'type' } }, abbr: 'abbr', address: 'address', area: 'area', article: 'article', aside: 'aside', audio: { name: 'audio', attributes: { autoPlay: 'autoplay', autoBuffer: 'autobuffer', buffered: 'buffered', controls: 'controls', loop: 'loop', muted: 'muted', played: 'played', preload: 'preload', src: 'src', volume: 'volume' } }, blockquote: 'blockquote', b: 'b', base: 'base', bdi: 'bdi', bdo: 'bdo', br: 'br', button: { name: 'button', attributes: { autoFocus: 'autofocus', disabled: 'disabled', form: 'form', formAction: 'formaction', formMethod: 'formmethod', formType: 'formtype', formValidate: 'formvalidate', formTarget: 'formtarget', type: 'type', value: 'value', } }, canvas: { name: 'canvas', attributes: { height: 'height', width: 'width' } }, caption: 'caption', cite: 'cite', code: 'code', col: 'col', colgroup: 'colgroup', data: { name: 'data', attributes: { value: 'value' } }, datalist: 'datalist', dfn: 'dfn', div: 'div', dd: 'dd', del: 'del', details: { name: 'details', attributes: { open: 'open' } }, dl: 'dl', dt: 'dt', em: 'em', embed: { name: 'embed', attributes: { height: 'height', src: 'src', type: 'type', width: 'width', } }, fieldset: { name: 'fieldset', attributes: { disabled: 'disabled', form: 'form', name: 'name' } }, figcaption: 'figcaption', figure: 'figure', footer: 'footer', form: { name: 'form', attributes: { acceptCharset: 'accept-charset', action: 'action', autocomplete: 'autocomplete', enctype: 'enctype', method: 'method', name: 'name', noValidate: 'novalidate', target: 'target', } }, h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6', head: 'head', header: 'header', hgroup: 'hgroup', hr: 'hr', i: 'i', input: { name: 'input', attributes: { accept: 'accept', autoFocus: 'autofocus', autoComplete: 'autocomplete', checked: 'checked', disabled: 'disabled', form: 'form', formAction: 'formaction', formMethod: 'formmethod', formType: 'formtype', formValidate: 'formvalidate', formTarget: 'formtarget', height: 'height', list: 'list', max: 'max', maxLength: 'maxlength', min: 'min', minLength: 'minlength', multiple: 'multiple', name: 'name', placeholder: 'placeholder', readOnly: 'readonly', required: 'required', size: 'size', src: 'src', step: 'step', type: 'type', value: 'value', width: 'width', } }, img: { name: 'img', attributes: { alt: 'alt', crossOrigin: 'crossorigin', height: 'height', isMap: 'ismap', longDesc: 'longdesc', referrerPolicy: 'referrerpolicy', sizes: 'sizes', src: 'src', srcset: 'srcset', width: 'width', useMap: 'usemap', } }, ins: 'ins', kbd: 'kbd', label: { name: 'label', attributes: { htmlFor: 'for' } }, legend: 'legend', li: 'li', link: 'link', main: 'main', map: { name: 'map', attributes: { name: 'name' } }, mark: 'mark', meta: 'meta', meter: { name: 'meter', attributes: { form: 'form', high: 'high', low: 'low', min: 'min', max: 'max', optimum: 'optimum', value: 'value', } }, nav: 'nav', ol: 'ol', object: { name: 'object', attributes: { form: 'form', height: 'height', name: 'name', type: 'type', typeMustmatch: 'typemustmatch', useMap: 'usemap', width: 'width', } }, optgroup: { name: 'optgroup', attributes: { disabled: 'disabled', label: 'label' } }, option: { name: 'option', attributes: { disabled: 'disabled', label: 'label', selected: 'selected', value: 'value' } }, output: { name: 'output', attributes: { htmlFor: 'for', form: 'form', name: 'name' } }, p: 'p', param: { name: 'param', attributes: { name: 'name', value: 'value' } }, pre: 'pre', progress: { name: 'progress', attributes: { max: 'max', value: 'value', } }, rp: 'rp', rt: 'rt', rtc: 'rtc', ruby: 'ruby', s: 's', samp: 'samp', section: 'section', select: { name: 'select', attributes: { autoFocus: 'autofocus', disabled: 'disabled', form: 'form', multiple: 'multiple', name: 'name', required: 'required', size: 'size', } }, small: 'small', source: { name: 'source', attributes: { media: 'media', sizes: 'sizes', src: 'src', srcset: 'srcset', type: 'type', } }, span: 'span', strong: 'strong', style: 'style', sub: 'sub', sup: 'sup', table: 'table', tbody: 'tbody', th: 'th', thead: 'thead', textarea: { name: 'textarea', attributes: { autoComplete: 'autocomplete', autoFocus: 'autofocus', cols: 'cols', disabled: 'disabled', form: 'form', maxLength: 'maxlength', minLength: 'minlength', name: 'name', placeholder: 'placeholder', readOnly: 'readonly', required: 'required', rows: 'rows', selectionDirection: 'selectionDirection', wrap: 'wrap', } }, td: 'td', tfoot: 'tfoot', tr: 'tr', track: { name: 'track', attributes: { htmlDefault: 'default', kind: 'kind', label: 'label', src: 'src', srclang: 'srclang' } }, time: 'time', title: 'title', u: 'u', ul: 'ul', video: { name: 'video', attributes: { autoPlay: 'autoplay', buffered: 'buffered', controls: 'controls', crossOrigin: 'crossorigin', height: 'height', loop: 'loop', muted: 'muted', played: 'played', poster: 'poster', preload: 'preload', src: 'src', width: 'width' } }, } const GLOBAL_ATTRIBUTES = { accessKey: 'accesskey', className: 'class', contentEditable: 'contenteditable', contextMenu: 'contextmenu', dir: 'dir', draggable: 'draggable', dropZone: 'dropzone', hidden: 'hidden', id: 'id', itemId: 'itemid', itemProp: 'itemprop', itemRef: 'itemref', itemScope: 'itemscope', itemType: 'itemtype', lang: 'lang', spellCheck: 'spellcheck', tabIndex: 'tabindex', title: 'title', translate: 'translate', } const EVENT_HANDLERS = { onClick: 'click', onFocus: 'focus', onBlur: 'blur', onChange: 'change', onSubmit: 'submit', onInput: 'input', onResize: 'resize', onScroll: 'scroll', onWheel: 'mousewheel', onMouseDown: 'mousedown', onMouseUp: 'mouseup', onMouseDown: 'mousedown', onMouseMove: 'mousemove', onMouseEnter: 'mouseenter', onMouseOver: 'mouseover', onMouseOut: 'mouseout', onMouseLeave: 'mouseleave', onTouchStart: 'touchstart', onTouchEnd: 'touchend', onTouchCancel: 'touchcancel', onContextMenu: 'Ccntextmenu', onDoubleClick: 'dblclick', onDrag: 'drag', onDragEnd: 'dragend', onDragEnter: 'dragenter', onDragExit: 'dragexit', onDragLeave: 'dragleave', onDragOver: 'dragover', onDragStart: 'Dragstart', onDrop: 'drop', onLoad: 'load', onCopy: 'copy', onCut: 'cut', onPaste: 'paste', onCompositionEnd: 'compositionend', onCompositionStart: 'compositionstart', onCompositionUpdate: 'compositionupdate', onKeyDown: 'keydown', onKeyPress: 'keypress', onKeyUp: 'keyup', onAbort: 'Abort', onCanPlay: 'canplay', onCanPlayThrough: 'canplaythrough', onDurationChange: 'durationchange', onEmptied: 'emptied', onEncrypted: 'encrypted ', onEnded: 'ended', onError: 'error', onLoadedData: 'loadeddata', onLoadedMetadata: 'loadedmetadata', onLoadStart: 'Loadstart', onPause: 'pause', onPlay: 'play ', onPlaying: 'playing', onProgress: 'progress', onRateChange: 'ratechange', onSeeked: 'seeked', onSeeking: 'seeking', onStalled: 'stalled', onSuspend: 'suspend ', onTimeUpdate: 'timeupdate', onVolumeChange: 'volumechange', onWaiting: 'waiting', onAnimationStart: 'animationstart', onAnimationEnd: 'animationend', onAnimationIteration: 'animationiteration', onTransitionEnd: 'transitionend' } function createElement(tagName, props = {}, ...childNodes) { if (props === null) { props = {} } const tag = HTML_TAGS[tagName] const object = typeof tag === 'object' const localAttrs = object ? tag.attributes || {} : {} const attrs = Object.assign({}, GLOBAL_ATTRIBUTES, localAttrs) const tagType = object ? tag.name : tag const el = document.createElement(tagType) Object.keys(props).forEach(prop => { if (prop in attrs) { el.setAttribute(attrs[prop], props[prop]) } if (prop in EVENT_HANDLERS) { el.addEventListener(EVENT_HANDLERS[prop], props[prop]) } }) if ('style' in props) { const styles = props.style Object.keys(styles).forEach(prop => { const value = styles[prop] if (typeof value === 'number') { el.style[prop] = `${value}px` } else if (typeof value === 'string') { el.style[prop] = value } else { throw new Error(`Expected "number" or "string" but received "${typeof value}"`) } }) } childNodes.forEach(childNode => { if (typeof childNode === 'object') { el.appendChild(childNode) } else if (typeof childNode === 'string') { el.appendChild(document.createTextNode(childNode)) } else { throw new Error(`Expected "object" or "string" but received "${typeof value}"`) } }) return el }
这里的createElement仅是对一个单独对object可以进行处理,想要真正完全复原html结构,还需要你对整个virtual dom进行遍历处理。这时你反过来去看输出结果,就会觉得有问题,items.map那个地方是不是有毛病?这里有两个地方需要调整一下,一个是babel-plugin-syntax-jsx这个插件,其实我们并不需要它,因为babel-plugin-transform-jsx这个插件已经让babel支持JSX语法了。第二个是,我们需要修改.babelrc把createElement加进去:
{ "plugins": [["transform-jsx", { "function": "createElement" }]] }
这样之后,输出的结果将会是:
感觉离react进了好多,这个结果里面,createElement被认为是一个全局函数,而如果你想把它放在一个文件里面,你只需要在tpl.js里面把createElement import进来就可以了,就像react的组件做的那样。回到这里,我们会发现,其实React.createElement已经帮我们完成了所有的事,而不需要我们自己再去写很多代码来实现。
小结
这篇文章从编译的角度重新带你去认识jsx,让你可以尝试在自己的项目中使用jsx作为模板引擎。而且结合virtual dom的知识,可以逐渐搭建起自己的virtual dom引擎。
2018-05-21 11740 JSX, Virtual DOM
为什么组件解析出来的不对啊
“`code
function App() {
return (
App
)
}
console.log()
“`
{
“elementName”: “App”,
“attributes”: {},
“children”: null
}
我艹这个防止 XSS 的给我标签搞掉了
怎么实现数据响应
数据响应是框架实现的,你可以用react也可以用vue,或者自己写一个数据驱动的框架
哈咯作者大大,想问下示例里的
$ npm install babel-cli babel-core babel-p>
代码没法跑,运行会报错。请教下该如何解决呢~不胜感谢!
报错 >>>
zsh: parse error near `\n’
改好了,可能之前提交文章的时候手抖干掉了
有问题想请教一下,方便加下微信么
有什么问题可以直接留言
没清楚/** @jsx createElement */这个文件怎么使用,并且怎么渲染出来
这个文件定义了一个全局的 createElement 函数。
jsx 文件编译结果中也有一个 createElement 函数,这个函数就是使用前面定义的全局函数。
怎么用HTML渲染出来呢
渲染当然是靠react或者其他什么渲染库来做了。jsx是virtual dom的描述语法糖,不是渲染语法。这篇文章是告诉你怎么拿到 jsx 的结果(也就是virtual dom),不是告诉你怎么渲染。