Day 1: Starting small.
Intro
Linus Torvalds开始尝试写的第一个Linux版本只能做一件事,就是在一台386裸机上反复打印出“AAAA”与“BBBB”的文本。当然用汇编语言来实现这个工作并不容易,包括实现实模式到保护模式的跳转,并且通过一个Intel 8253定时器触发中断实现进程的切换。很多精巧的设计与思考不一定会反映在作品最初的形态上,但是会在未来演化的进程中逐步体现。
于是二话不说我们就先写了一段非常简单的代码,打开文本编辑器粘贴进去并保存为index.html,然后通过浏览器打开。当然你即使不这么做也清楚你会看到什么。就好像一个作曲家只需要看到乐谱,头脑中各个乐器与声部就会开始演奏起来。
<html>
<head>
</head>
<body>
<div id="root"></div>
</body>
<script>
let yay = document.createElement('p');
yay.appendChild(document.createTextNode('yay'));
document.getElementById('root').appendChild(yay);
</script>
</html>
为了用上JSX而进行的准备工作
有可能是出于可读性的考虑,DOM的属性及方法名称都非常繁琐。如果大部分创建DOM对象的工作都要像这样进行,肯定会非常费键盘。所以马上考虑搞一个自定义的JSX。如果你用了一段时间React肯定会对JSX非常熟悉,那么现在我们来做一些工程准备:
npm init
npm i --save webpack
npm i --save babel-loader @babel/core @babel/preset-env\
@babel/plugin-transform-react-jsx @babel/plugin-proposal-class-properties
在目录下建立一个webpack.config.js并这么写
// webpack.config.js
const path = require('path')
module.exports = {
entry: path.resolve(path.resolve(__dirname), 'src', 'index.js'),
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: './dist'
},
output: {
filename: 'dist.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'umd'
},
module: {
rules: [{
test: /\.js$/,
loader: "babel-loader",
}]
},
}
然后建立一个.babelrc文件,我们的自定义JSX就全指它了。
// .babelrc
{
"presets": [
"@babel/preset-env"
],
"plugins": [
["@babel/plugin-transform-react-jsx", {
"pragma": "createElem",
"pragmaFrag": "createElemFrag",
}],
"@babel/plugin-proposal-class-properties"
]
}
稍微解释一下这些preset和plugin:
preset-env: 为我们提供了非常基础的ES6翻译plugin-proposal-class-properties: 提供了如在class中以箭头函数来命名方法,从而避免你需要手工.bind(this)的工作plugin-transform-react-jsx: 两个属性pragma和pragmaFrag将它从React自带的处理JSX的方法变为了我们自己的方法createElem与createElemFrag。具体讲解自定义JSX的方法有很多,此处不再赘述,我们只需要知道,我们得自己写一个createElem方法才能处理JSX。
我们按照webpack.config.js中定义的那样,把我们之前写在index.html中的代码部分挪到了./src/index.js里。于是这些文件变成了这样:
<!-- index.html -->
<html>
<head>
<script src="./dist/dist.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
// index.js
let yay = document.createElement('p');
yay.appendChild(document.createTextNode('yay'));
document.getElementById('root').appendChild(yay);
JSX的真正出场
把index.js改成这样:
//index.js
function createElem(tag, attrs, ...children){
return {tag, attrs, children};
}
let JSXElem = <div someAttr='awesome' yo='wasup man'>
<p>yay</p>
</div>;
console.log(JSON.stringify(JSXElem, null, 2));
我们在控制台上看到了:
{
"tag": "div",
"attrs": {
"some-attr": "awesome",
"yo": "wasup man"
},
"children": [
{
"tag": "p",
"attrs": null,
"children": [
"yay"
]
}
]
}
不难发现以下特点:
- DOM对象的标签映射到
createElem的第一个argument - DOM对象所有的属性被打包成一个object映射到
createElem的第二个argument - DOM对象的所有children被展开送给了
createElem后面所有的arguments,当然被我们用spread operator收集回来了。 - 如果DOM应该是一个TextNode,那么会返回一个字符串,而不是包含
tag,attrs和children的完整结构 - 对于嵌套的DOM对象,createElem进行的是先序遍历,它先将子层的对象进行了处理,然后才执行了父层的createElem。换句话说,当父层的createElem被调用时,从第三个参数起的参数都是子层createElem调用执行后的返回值。
现在我们的createElem还只能打印一个JSON string,我们需要让它能返回实际的DOM对象,然后在页面中渲染出来。于是我们对它进行如下改进:
function createElem(tag, attrs, ...children){
let elem = document.createElement(tag);
attrs = attrs || {};
// looking forward to ?? operator for null coalescing
for (let attrName in attrs)
elem.setAttribute(attrName, attrs[attrName]);
for (let child of children) (typeof child === 'string')
? elem.appendChild(document.createTextNode(child))
: elem.appendChild(child);
return elem;
}
let elem = <div some-attr='awesome' yo='wasup man'><p>yay</p></div>;
console.log(elem);
document.getElementById('root').appendChild(elem);
经历了一番周折,我们终于看到了和最初版本一样的效果,但是接下来我们会针对createElem开展一系列的工作,使它能做更多的事。
Day 2: createElem的进化
今天的幸福生活,有很大程度上是选择器给我们带来的。在选择器尚未成为DOM今日Web技术标准时,是John Resig的jQuery帮了我们大忙。起初一个网页就像是一页纸,一切都是静态内容。当网页被赋予越来越多的动态交互特性时,我们就需要找到元素,并修改它。