通用组件的演化

266 阅读4分钟

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: 两个属性pragmapragmaFrag将它从React自带的处理JSX的方法变为了我们自己的方法createElemcreateElemFrag。具体讲解自定义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"
      ]
    }
  ]
}

不难发现以下特点:

  1. DOM对象的标签映射到createElem的第一个argument
  2. DOM对象所有的属性被打包成一个object映射到createElem的第二个argument
  3. DOM对象的所有children被展开送给了createElem后面所有的arguments,当然被我们用spread operator收集回来了。
  4. 如果DOM应该是一个TextNode,那么会返回一个字符串,而不是包含tag, attrschildren的完整结构
  5. 对于嵌套的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帮了我们大忙。起初一个网页就像是一页纸,一切都是静态内容。当网页被赋予越来越多的动态交互特性时,我们就需要找到元素,并修改它