美文网首页
500 lines or less学习笔记(十三)——Web 电

500 lines or less学习笔记(十三)——Web 电

作者: 简单一点点 | 来源:发表于2021-12-20 21:47 被阅读0次

    500lines中最小的项目,99行实现一个Web电子表格,主要借助了AngularJS框架,可以学习下。

    作者

    Audrey Tang,自学成才的程序员和翻译,Audrey作为云服务本地化和自然语言技术的独立承包商与苹果合作。Audrey以前设计并领导过第一个 Perl 6 实现,并在 Haskell、Perl 5 和 Perl 6 的计算机语言设计委员会任职。目前,Audrey 是一个全职的g0v贡献者和领导台湾省的第一个电子规则制定项目。本文介绍一个 Web 电子表格,它是用 Web 浏览器支持的三种语言(HTML、JavaScript和CSS)编写的,共99行。

    引言

    1990 年,Tim Berners-Lee 发明了全球资讯网,当时的网页文件(Web pages)都是以 HTML 写成,使用尖括号标记(tags)来标记文字,给内容安排逻辑结构。以 <a>…</a> 标记的文字会变成超链接(hyperlinks),把用户导引至其他网页。

    在 20 世纪 90 年代,浏览器加入了各种展示性标记到 HTML 词汇表,包括一些非标准标记,例如来自 Netscape Navigator 的 <blink>…</blink> 和来自 Internet Explorer 的 <marquee>…</marquee>,在可用性和浏览器兼容性方面造成了广泛的问题。

    为了将 HTML 限制在描述文档逻辑结构的原始目的上,浏览器开发者最后同意支持两种附加语言:CSS 来形容网页的展示风格,以及 JS 来描述其动态互动功能。

    从那时开始,这三种程式语言经过了 20 年的共同进化,已经变得更加简洁和强大。JS 引擎的效能获得高度提升,使得大规模的 JS 框架开始盛行,例如 AngularJS。

    如今,跨平台的应用网站(Web applications,例如电子部表格),已经跟上个世纪的桌面应用程序(如 VisiCalc、Lotus 1-2-3 和 Excel)一样普及了。

    使用 AngularJS 的网页应用可以在 99 行里面提供多少功能?让我们来看看!

    概述

    在 spreadsheet 目录里,包含了三种 Web 程式语言在 2014 年末版本的展示范例:描述结构的 HTML5、描述展示风格的 CSS3,以及描述互动功能的 JS ES6 “Harmony” 。它也用到 Web Storage 来保存资料,以及利用 Web Worker 在后台运行 JS 代码。在撰写本文时,这些 Web 标准都已获得 Firefox、Chrome、Internet Explorer 11+,以及移动浏览器 iOS 5+ 和 Android 4+ 的支持。

    让我们在浏览器中打开 spreadsheet:

    01-initial.png

    基本概念

    电子表格跨越两个维度,列从A开始,行从1开始。每个单元格都有一个唯一的坐标(如A1)和内容(如“1874”),属于以下四种类型之一:

    • 文本:B1中的 +D1中的“⇒”,左对齐。

    • 数字:A1中的“1874”和C1中的“2046”,向右对齐。

    • 公式:E1中的=A1+C1,计算值为“3920”,以浅蓝色背景显示。

    • 空:第2行中的所有单元格当前都为空。

    单击“3920”将焦点设置为E1,并在输入框中显示其公式。

    02-input.png

    现在让我们将焦点设置在 A1 上,并将其内容更改为“1”,从而使 E1 将其值重新计算为“2047”。

    03-changed.png

    ENTER 键将焦点设置为 A2 并将其内容更改为 =Date(),然后按 TAB 键,将B2的内容更改为=alert(),然后再次按 TAB 键将焦点设置为 C2

    04-error.png

    这表明一个公式的结果可以是一个数字(E1中的“2047”)、一个文本(A2中的当前时间,向左对齐)或一个错误(B2中的红色字母,居中对齐)。

    接下来,让我们尝试输入 =for(;;){},永不终止的无限循环的JS代码。电子表格将通过在尝试更改后自动恢复 C2 的内容来防止这种情况。

    现在使用 Ctrl-RCmd-R 在浏览器中重新加载页面,以验证电子表格内容是否持久,在浏览器会话中保持不变。要将电子表格重置为其原始内容,请按左上角的 按钮。

    渐进增强

    在深入研究99行代码之前,有必要在浏览器中禁用JS,重新加载页面,并注意差异:

    • 屏幕上只保留一个2x2表格,只有一个内容单元格,而不是一个大表格。

    • 行和列标签被 {{Row}}{{col}} 替换。

    • 按钮不起作用。

    • TAB 键或单击内容的第一行仍会显示一个可编辑的输入框。

    05-nojs.png

    当我们禁用动态交互(JS)时,内容结构(HTML)和表示样式(CSS)仍然有效。如果一个网站在 JS 和 CSS 都被禁用的情况下仍然可用,我们说它坚持渐进增强原则,使它的内容能够被最多的读者访问。

    因为我们的电子表格是一个没有服务器端代码的 Web 应用程序,所以我们必须依赖 JS 来提供所需的逻辑。但是,当 CSS 没有完全获得支持时,它确实可以正常工作,比如屏幕阅读器和文本模式浏览器。

    06-nocss.png

    如上图所示,如果在浏览器中启用 JS 并禁用 CSS,则效果如下:

    • 所有背景和前景颜色都消失了。

    • 输入框和单元格值都显示,而不是只显示一个。

    • 除此之外,应用程序仍然与完整版本相同。

    代码走读

    下面的图显示了HTML和JS组件之间的关联。

    00-architecture.png

    为了理解这个图,让我们按照浏览器加载它们的顺序来浏览这四个源代码文件。

    • index.html: 19行
    • main.js: 38行(不包括注释和空行)
    • worker.js: 30行(不包括注释和空行)
    • styles.css: 12行

    HTML

    index.html 中的第一行声明它是用带有UTF-8编码的 HTML5 的:

    <!DOCTYPE html><html><head><meta charset="UTF-8">
    

    如果没有字符集声明,浏览器可能会将重置按钮的Unicode符号显示为↻, 也就是乱码:由解码问题导致的错误文本。

    接下来的三行是 JS 声明,通常放在head部分中:

    <script src="lib/angular.js"></script>
    <script src="main.js"></script>
    <script>
        try { angular.module('500lines') }
        catch(e){ location="es5/index.html" }
    </script>
    

    <script src="…"> 标记从与 HTML 页面相同的路径加载 JS 资源。例如,如果当前URL为 http://abc.com/x/index.html,则 lib/angular.js 引用 http://abc.com/x/lib/angular.js

    try{ angular.module('500lines') }测试 main.js 是否正确加载;如果没有,它会告诉浏览器导航到 es5/index.html。这种基于重定向的优雅降级技术确保了对于不支持ES6的2015年以前的浏览器,我们可以将 JS 程序解析成 ES5 版本作为回退。

    接下来的两行加载 CSS 资源,关闭 head 部分,然后开始包含用户可见部分的 body 部分:

    <link href="styles.css" rel="stylesheet">
    </head>
    <body ng-app="500lines" ng-controller="Spreadsheet" ng-cloak>
    

    上面的 ng-appng-controller 属性告诉 AngularJS 调用 500lines 模块的电子表格函数,该函数将返回一个模型:一个在文档视图上提供绑定的对象(ng-cloak 属性在绑定就位之前隐藏文档以防显示。)

    作为一个具体的例子,当用户单击下一行中定义的 <button> 时,其 ng-click 属性将触发并调用 reset()calc(),这是 JS 模型提供的两个命名函数:

      <table><tr>
        <th><button type="button" ng-click="reset(); calc()">↻</button></th>
    

    下一行使用 ng-repeat 在顶行显示列标签列表:

    <th ng-repeat="col in Cols">{{ col }}</th>
    

    例如,如果 JS 模型将 Cols 定义为 ["A","B","C"],那么将有三个标题单元格(th)相应地标记。{{col}}告诉 AngularJS 插入表达式,用 col的当前值填充每个 th 中的内容。

    类似地,接下来的两行遍历 Rows 中的值[1,2,3]等等,为每个创建一行,并用其编号标记最左侧的第 th 个单元格:

      </tr><tr ng-repeat="row in Rows">
        <th>{{ row }}</th>
    

    由于 <tr ng-repeat> 标记尚未由 </tr> 关闭,因此 row 变量仍然可用于表达式。下一行在当前行中创建一个数据单元(td),并在其 ng-class 属性中使用 colrow 变量:

        <td ng-repeat="col in Cols" ng-class="{ formula: ('=' === sheet[col+row][0]) }">
    

    这里有几个重点。在 HTML 中,class属性描述了一组类名,这些类名允许 CSS 对它们进行不同的样式设置。这里的 ng-class 计算表达式 ('=' === sheet[col+row][0]);如果为 true,则 <td>formula 作为一个附加类获取,该类为单元格提供淡蓝色背景,如styles.css的第8行中使用 .formula 类选择器定义的那样。

    上面的表达式通过测试 = 是否是 sheet[col+row] 中字符串的初始字符([0])来检查当前单元格是否是公式,其中 sheet 是一个JS模型对象,坐标(如"E1")是属性,单元格内容(如"=A1+C1")是值。请注意,因为 col 是字符串而不是数字,所以 col+row 中的 + 表示串联而不是加法。

    <td> 中,我们为用户提供了一个输入框,用于编辑存储在 sheet[col+row] 中的单元格内容:

           <input id="{{ col+row }}" ng-model="sheet[col+row]" ng-change="calc()"
            ng-model-options="{ debounce: 200 }" ng-keydown="keydown( $event, col, row )">
    

    这里的关键属性是 ng-model,它支持 JS 模型和输入框的可编辑内容之间的双向绑定。实际上,这意味着每当用户在输入框中进行更改时,JS模型将更新 sheet[col+row] 以匹配内容,并触发其 calc() 函数以重新计算所有公式单元格的值。

    为了避免在用户按住某个键时重复调用 calc()ng-model-options 将更新速率限制为每 200 毫秒一次。

    此处的 id 属性用坐标 col+row 取值。HTML元素的id属性必须与同一文档中所有其他元素的id不同。这确保 #A1 ID 选择器引用单个元素,而不是类选择器 .formular 之类的元素集。当用户按下 UP/DOWN/ENTER 时,keydown() 中的键盘导航逻辑将使用ID选择器来确定要重点关注哪个输入框。

    在输入框之后,我们放置一个 <div> 来显示当前单元格的计算值,在JS模型中由对象 errsvals 表示:

          <div ng-class="{ error: errs[col+row], text: vals[col+row][0] }">
            {{ errs[col+row] || vals[col+row] }}</div>
    

    如果在计算公式时发生错误,文本插值将使用 errs[col+row] 中包含的错误消息,ng-classerror 类应用于元素,从而允许CSS以不同的方式对其进行样式设置(使用红色字母、与中心对齐等)。

    如果没有错误,|| 右侧的 vals[col+row] 将被取值。如果是非空字符串,则初始字符([0])将计算为 true,并将 text 类应用于左对齐文本的元素。

    因为空字符串和数值没有初始字符,ng-class 不会为它们分配任何类,所以 CSS 可以将它们的样式设置为默认情况下的右对齐方式。

    最后,我们用 </td> 关闭列级的 ng-repeat 循环,用 </tr> 关闭行级循环,并用以下命令结束 HTML 文档:

        </td>
      </tr></table>
    </body></html>
    

    JS: 主控制层

    main.js 文件根据 index.html 中的 <body> 元素的要求定义 500 行模块及其电子表格控制器功能。

    作为 HTML 视图和后台工作层之间的桥梁,它有四个任务:

    • 定义列和行的数量和标题。

    • 为键盘移动和重置按钮提供事件处理程序。

    • 当用户更改电子表格时,将其新内容发送给工作人员。

    • 当计算结果从工作层到达时,更新视图并保存当前状态。

    下图中的流程图更详细地显示了控制层与工作层的交互:

    00-flowchart.png

    现在让我们浏览一下代码。在第一行中,我们请求AngularJS 的 $scope

    angular.module('500lines', []).controller('Spreadsheet', function ($scope, $timeout) {
    

    $scope 中的 $ 是变量名的一部分。这里我们还从 AngularJS 请求 $timeout 服务函数;稍后,我们将使用它来防止无限循环公式。

    要将 ColsRows 放入模型中,只需将它们定义为 $scope 的属性:

      // Begin of $scope properties; start with the column/row labels
      $scope.Cols = [], $scope.Rows = [];
      for (col of range( 'A', 'H' )) { $scope.Cols.push(col); }
      for (row of range( 1, 20 )) { $scope.Rows.push(row); }
    

    ES6 for...of 语法可以很容易地在具有起点和终点的范围内循环,辅助函数 range 定义为生成器:

    function* range(cur, end) { while (cur <= end) { yield cur;
    

    上面的 function* 意味着 range 返回一个迭代器,其中有一个 while 循环,每次只 yield 一个值。每当 for 循环需要下一个值时,它将在 yield 行之后立即恢复执行:

        // If it’s a number, increase it by one; otherwise move to next letter
        cur = (isNaN( cur ) ? String.fromCodePoint( cur.codePointAt()+1 ) : cur+1);
      } }
    

    为了生成下一个值,我们使用 isNaN 来查看 cur 是否意味着一个字母(NaN代表“不是一个数字”),如果是字母,我们得到字母的码点值,将其递增1,然后将码点值转换回下一个字母。否则,我们只需将数字增加1。

    接下来,我们定义 keydown() 函数来处理跨行的键盘导航:

      // UP(38) and DOWN(40)/ENTER(13) move focus to the row above (-1) and below (+1).
      $scope.keydown = ({which}, col, row)=>{ switch (which) {
    

    箭头函数从 <input ng keydown> 接收参数 ($event, col, row),使用析构分配将 $event.which 赋值到 which 参数中,并检查它是否在三个导航键代码中:

      case 38: case 40: case 13: $timeout( ()=>{
    

    如果是,我们使用 $timeout 在当前 ng-keydownng-change 处理程序之后安排焦点更改。因为 $timeout 需要一个函数作为参数,所以 ()=>{…} 语法构造了一个函数来表示焦点更改逻辑,它首先检查移动方向:

          const direction = (which === 38) ? -1 : +1;
    

    const 声明符意味着在函数执行期间 direction 不会改变。如果键码为38(向上),则移动方向为向上(-1,从A2A1),否则为向下(+1,从A2A3)。

    接下来,我们使用ID选择器语法(例如 "#A3")检索目标元素,该语法由一个模板字符串构成,该字符串写在一对反引号中,连接前导 #、当前列和目标 row + direction

          const cell = document.querySelector( `#${ col }${ row + direction }` );
          if (cell) { cell.focus(); }
        } );
      } };
    

    我们对 querySelector 的结果进行了额外的检查,因为从A1向上移动将产生选择器 #A0,它没有相应的元素,因此不会触发焦点更改—在最下面一行按向下键也是如此。

    接下来,我们定义 reset() 函数,以便重置按钮可以还原工作表的内容:

      // Default sheet content, with some data cells and one formula cell.
      $scope.reset = ()=>{ 
        $scope.sheet = { A1: 1874, B1: '+', C1: 2046, D1: '->', E1: '=A1+C1' }; }
    

    init() 函数尝试从 localStorage 中恢复 sheet 内容的以前状态,如果是首次运行应用程序,则默认为初始内容:

      // Define the initializer, and immediately call it
      ($scope.init = ()=>{
        // Restore the previous .sheet; reset to default if it’s the first run
        $scope.sheet = angular.fromJson( localStorage.getItem( '' ) );
        if (!$scope.sheet) { $scope.reset(); }
        $scope.worker = new Worker( 'worker.js' );
      }).call();
    

    在上面的 init()函数中,有些东西需要关注:

    • 我们使用 ($scope.init = ()=>{…}).call() 语法来定义函数并立即调用它。

    • 因为 localStorage 只存储字符串,所以我们使用 angular.fromJson() 从 JSON 表示形式解析 sheet 结构。

    • init() 的最后一步,我们创建了一个新的 Web 工作线程,并将其分配给 worker 范围属性。尽管 worker 不是直接在视图中使用的,但是通常使用 $scope来共享模型函数之间使用的对象,在这里是 init() 和下面的 calc() 之间。

    sheet 保存用户可编辑的单元格内容时,errsvals 包含用户只读的计算结果(错误和值):

      // Formula cells may produce errors in .errs; normal cell contents are in .vals
      [$scope.errs, $scope.vals] = [ {}, {} ];
    

    有了这些属性,我们可以定义 calc() 函数,每当用户更改工作表时,该函数都会触发:

      // Define the calculation handler; not calling it yet
      $scope.calc = ()=>{
        const json = angular.toJson( $scope.sheet );
    

    在这里,我们对 sheet 的状态进行快照,并将其存储在常量 json(一个JSON字符串)中。接下来,我们从 $timeout构造一个 promise,如果花费的时间超过99毫秒,它将取消即将进行的计算:

        const promise = $timeout( ()=>{
          // If the worker has not returned in 99 milliseconds, terminate it
          $scope.worker.terminate();
          // Back up to the previous state and make a new worker
          $scope.init();
          // Redo the calculation using the last-known state
          $scope.calc();
        }, 99 );
    

    由于我们确保通过HTML中的 <input ng-model-options>属性,calc()最多每200毫秒调用一次,因此这种安排为 init()留出101毫秒的时间来将 sheet 恢复到最后一个已知的良好状态,并生成一个新的工作进程。

    工作线程的任务是根据工作表的内容计算 errsvals。因为main.js和worker.js是通过消息传递进行通信的,所以我们需要一个 onmessage 处理程序来接收准备好的结果:

        // When the worker returns, apply its effect on the scope
        $scope.worker.onmessage = ({data})=>{
          $timeout.cancel( promise );
          localStorage.setItem( '', json );
          $timeout( ()=>{ [$scope.errs, $scope.vals] = data; } );
        };
    

    如果调用 onmessage,我们知道 json 中的工作表快照是稳定的(即,不包含无限循环公式),因此我们取消99毫秒超时,将快照写入localStorage,并使用 $timeout 函数计划UI更新,该函数将 errsvals 更新到用户可见视图。

    处理程序就位后,我们可以将工作表的状态发布到工作线程,并在后台开始计算:

        // Post the current sheet content for the worker to process
        $scope.worker.postMessage( $scope.sheet );
      };
    
      // Start calculation when worker is ready
      $scope.worker.onmessage = $scope.calc;
      $scope.worker.postMessage( null );
    });
    

    JS:后台工作线程

    使用 Web 工作线程来计算公式,而不是使用 JS 主线程来执行任务,有三个原因:

    • 当工作线程在后台运行时,用户可以继续与电子表格交互,而不会被主线程中的计算阻塞。

    • 因为我们接受公式中的任何 JS 表达式,所以工作线程提供了一个沙盒,防止公式干扰包含它们的页面,例如弹出alert() 对话框。

    • 公式可以引用任何坐标作为变量。其它坐标可能包含另一个以循环引用结尾的公式。为了解决这个问题,我们使用工作线程的全局范围对象 self,并将这些变量定义为 self 上的 getter 函数来实现循环预防逻辑。

    有了这些认识后,让我们来看看工作线程的代码。

    工作线程的唯一目的是定义其 onmessage 处理程序。处理程序获取 sheet,计算 errsvals,并将它们发回主JS线程。我们首先在收到消息时重新初始化三个变量:

    let sheet, errs, vals;
    self.onmessage = ({data})=>{
      [sheet, errs, vals] = [ data, {}, {} ];
    

    为了将坐标转换为全局变量,我们首先使用 for...in 循环对 sheet 中的每个属性进行迭代:

      for (const coord in sheet) {
    

    ES6 引入 constlet 声明块范围的常量和变量;上面的 const coord 意味着在循环中定义的函数将在每次迭代中捕获 coord 的值。

    相反,JS的早期版本中的 var coord 会声明一个函数范围的变量,并且在每个循环迭代中定义的函数最终会指向同一个 coord 变量。

    通常,公式变量不区分大小写,并且可以选择使用 $ 前缀。因为 JS 变量是区分大小写的,所以我们使用 map检查同一坐标的四个变量名:

        // Four variable names pointing to the same coordinate: A1, a1, $A1, $a1
        [ '', '$' ].map( p => [ coord, coord.toLowerCase() ].map(c => {
          const name = p+c;
    

    注意上面的箭头函数语法:p => ...(p) => { ... } 相同。

    对于每个变量名(如 A1$a1),我们在 self上定义一个访问器属性,每当在表达式中计算时, 该属性都自动会计算 vals["A1"] 的值:

          // Worker is reused across calculations, so only define each variable once
          if ((Object.getOwnPropertyDescriptor( self, name ) || {}).get) { return; }
    
          // Define self['A1'], which is the same thing as the global variable A1
          Object.defineProperty( self, name, { get() {
    

    上面的 { get() { ... } } 语法是 { get: ()=>{ ... } } 的简写。因为我们只定义了 get 而没有定义 set,所以变量变成只读的,并且不能从用户提供的公式中修改。

    get 访问器从检查 vals[coord] 开始,如果已经计算了则返回它:

            if (coord in vals) { return vals[coord]; }
    

    如果不是,我们需要从 sheet[coord] 计算 vals[coord]

    首先我们将其设置为 NaN,这样像将A1设置为 =A1 这样的自引用将以 NaN 而不是无限循环结束:

            vals[coord] = NaN;
    

    接下来,我们检查 sheet[coord] 是否是一个数字,方法是将其转换为前缀为 + 的数字,将数字赋给 x,并将其字符串表示形式与原始字符串进行比较。如果它们不同,那么我们将 x 设置为原始字符串:

            // Turn numeric strings into numbers, so =A1+C1 works when both are numbers
            let x = +sheet[coord];
            if (sheet[coord] !== x.toString()) { x = sheet[coord]; }
    

    如果 x 的初始字符是 =,则它是一个公式单元格。我们使用 eval.call() 计算 = 后的部分,使用第一个参数 null 告诉 eval 在全局范围内运行,在计算中隐藏 xsheet 等词法范围变量:

            // Evaluate formula cells that begin with =
            try { vals[coord] = (('=' === x[0]) ? eval.call( null, x.slice( 1 ) ) : x);
    

    如果计算成功,结果将存储到 vals[coord] 中。对于非公式单元格,vals[coord] 的值仅为 x,可以是数字或字符串。

    如果 eval 导致错误,catch 块将测试是否是因为公式引用了 self 中尚未定义的空单元格:

            } catch (e) {
              const match = /\$?[A-Za-z]+[1-9][0-9]*\b/.exec( e );
              if (match && !( match[0] in self )) {
    

    在这种情况下,我们将缺少的单元格的默认值设置为“0”,清除 vals[coord],然后使用 self[coord] 重新运行当前计算:

                // The formula refers to a uninitialized cell; set it to 0 and retry
                self[match[0]] = 0;
                delete vals[coord];
                return self[coord];
              }
    

    如果用户稍后在 sheet[coord] 中为缺少的单元格提供内容,则 Object.defineProperty 将覆盖临时值。

    其他类型的错误存储在 errs[coord] 中:

              // Otherwise, stringify the caught exception in the errs object
              errs[coord] = e.toString();
            }
    

    如果出现错误,vals[coord] 的值将保持为 NaN,因为赋值没有完成执行。

    最后,get 访问器返回存储在 vals[coord] 中的计算值,该值必须是数字、布尔值或字符串:

            // Turn vals[coord] into a string if it's not a number or Boolean
            switch (typeof vals[coord]) { 
                case 'function': case 'object': vals[coord]+=''; 
            }
            return vals[coord];
          } } );
        }));
      }
    

    在为所有坐标定义了访问器之后,工作线程再次遍历坐标,使用 self[coord] 调用每个访问器,然后将生成的 errsvals 发回主 JS 线程:

      // For each coordinate in the sheet, call the property getter defined above
      for (const coord in sheet) { self[coord]; }
      return [ errs, vals ];
    }
    

    CSS

    styles.css 文件只包含几个选择器及其表示样式。首先,我们设置表格样式,将所有单元格边框合并在一起,相邻单元格之间不留空格:

    table { border-collapse: collapse; }
    

    标题和数据单元格都具有相同的边框样式,但我们可以通过它们的背景颜色来区分它们:标题单元格为浅灰色,默认情况下数据单元格为白色,公式单元格为浅蓝色背景:

    th, td { border: 1px solid #ccc; }
    th { background: #ddd; }
    td.formula { background: #eef; }
    

    对于每个单元格的计算值,显示的宽度是固定的。空单元格的高度最小,长线用尾部省略号剪裁:

    td div { text-align: right; width: 120px; min-height: 1.2em;
             overflow: hidden; text-overflow: ellipsis; }
    

    文本对齐和修饰由每个值的类型决定,如 texterror 类选择器是:

    div.text { text-align: left; }
    div.error { text-align: center; color: #800; font-size: 90%; border: solid 1px #800 }
    

    对于用户可编辑的 input 框,我们使用绝对定位将其覆盖在其单元格的顶部,并使其透明,以便具有单元格值的底层 div 通过以下方式显示:

    input { position: absolute; border: 0; padding: 0;
            width: 120px; height: 1.3em; font-size: 100%;
            color: transparent; background: transparent; }
    

    当用户在输入框上设置焦点时,它会跳入前台:

    input:focus { color: #111; background: #efe; }
    

    此外,底层 div 被折叠成一行,因此它完全被输入框覆盖:

    input:focus + div { white-space: nowrap; }
    

    结论

    由于本书建议500行或更少,用 99 行代码实现网络电子表格是一个最小的例子,请随时实验,并扩展到任何你喜欢的方向。

    以下是一些想法,在401行的剩余空间中很容易实现:

    • 使用ShareJS、AngularFire 或 GoAngular的协作在线编辑器。

    • 标记语法支持文本单元格,使用 angular-marked。

    • OpenFormula标准中的常用公式函数(SUM、TRIM等)。

    • 通过 SheetJS 与流行的电子表格格式(如 CSV 和SpreadsheetML)进行交互操作。

    • 导入和导出到在线电子表格服务,如 Google 电子表格和EtherCalc。

    JS版本说明

    本章旨在演示 ES6 中的新概念,因此我们使用 Traceur 编译器将源代码转换为ES5,以便在2015年以前的浏览器上运行。

    如果您希望直接使用 2010 版的JS,那么 as-javascript-1.8.5目录中的 main.js 和 worker.js 都是以 ES5 的样式编写的;源代码与具有相同行数的 ES6 版本一行一行进行了比较。

    对于喜欢更简洁语法的人,as-livescript-1.3.0 目录使用 livescript 而不是 ES6 来编写main.ls和worker.ls;它比JS版本短20行。

    基于LiveScript语言,as-react-livescript 目录使用 ReactJS 框架;它比同等的 AngularJS 长 10 行,但运行速度要快得多。

    相关文章

      网友评论

          本文标题:500 lines or less学习笔记(十三)——Web 电

          本文链接:https://www.haomeiwen.com/subject/pcxtqrtx.html