标签: navlang

  • 在任意上下文执行代码,`new Function` vs `eval()`

    在任意上下文执行代码,`new Function` vs `eval()`

    0. 需求及方向

    我厂的 Navigator 扩展遇到一个需求:

    1. 动态生成一段 JS 在浏览器里运行
    2. 能重复生成、重复运行
    3. 能生成一段新代码,继续在上一次的环境里运行

    其中(3)是新需求,(1)(2)已经比较完善地实现了。主要方案是使用 <script type="module">,这样加载的 JS 会自动执行,并且与全局环境相隔离,不会污染全局环境——这样就可以重复执行。

    (3)的难点在于,上一段 JS 已经执行过了,环境也自然释放了,我要么把上一阶段 JS 的执行结果保存下来,要么把执行环境保存下来。前者需要对 Navlang 编译器做大幅度的修改;后者则有很大的实施难度。

    经过研究和思考,我选择 保存环境 为攻关方向。即在上次执行结束前,在 window 上注册一个钩子函数,然后在后续追加执行的时候调用它,把后面的代码以函数的形式传进去,以便在上一次留下的环境里继续执行。

    1. 问题:访问环境里的变量

    添加钩子函数并不难,这样就行:

    // 在上一段代码最后添加钩子函数
    window.doNext = function (func) {
      func();
    }
    
    // 在新一段代码执行函数
    window.doNext(function () {
      // 新生成的函数
    });

    这样做虽然看起来是在上一次的上下文环境中执行新的函数,但由于 JS 的闭包和作用域链特性,实际执行时,新函数并不能访问到上一次上下文的数据。换言之,目的并没有实现。

    2. new Function() vs eval()

    然后回到标题。所以我们必须在上一次的环境里重新构建函数,这并不困难,其实开发浏览器扩展时,因为要协调四、五个运行环境,所以把函数序列化传输,再重新构建执行非常常见。于是我直接操起 new Function(),然后失败了。

    查阅 MDN,原来 new Function() 构造的函数,它的上下文会绑定在全局对象上(相当于 .call(null)),所以自然无法访问到上一次环境里的变量。

    再看 eval()文档,似乎可行,于是换用之,果然有效。至此,问题解决。

    3. 代码范例

    function navlangExecuteFunction(func) {
      // 使用模版字符串构建异步函数
      eval(`(async function doNext() {
        // 捕获中间可能出现的问题
        try {
          // 执行真正的函数
          await (${func})();
          // 为兼容 node.js,mock 一个 process 来处理执行结束
          process.exit();
        } catch (e) {
           if (!e.message.startsWith('Exit code:')) {
             console.error(e);
             process.exit(1);
           }
        }
      })()`);
    }
    window.navlangExecuteFunction = navlangExecuteFunction;
    const serializeFunction = (f) => {
      const serialized = f.toString();
      // Safari serializes async arrow functions with an invalid function keyword.
      // This needs to be removed in order for the function to be interpretable.
      const safariPrefix = 'async function ';
      if (serialized.startsWith(safariPrefix)) {
        const arrowIndex = serialized.indexOf('=>');
        const bracketIndex = serialized.indexOf('{');
        if (arrowIndex > -1 && (bracketIndex === -1 || arrowIndex < bracketIndex)) {
          return async ${serialized.slice(safariPrefix.length)};
        }
      }
      return serialized;
    };
    
    let nextStep = async function () {
      // 要执行的部分
    }
    nextStep = serializeFunction(nextStep);
    navlangExecuteFunction(nextStep);

    4. 可能带来的问题

    MDN 明确建议大家 不要用 eval。理由如下:

    1. 不安全。因为 eval() 会在当前环境执行代码,意味着攻击者可能窃取任何当前环境的数据。
    2. 性能差。现代 JS 引擎会对代码进行大量优化,包括转成机器码等。eval() 会破坏这个过程,使得运行性能大大降低。

    不过在我的场景下,这两个问题并不严重。一方面,被执行的追加代码都是由 Navlang 编译器生成,而不是任意第三方,它的安全性不会比其它的代码安全性低。另一方面,这个功能是帮助用户开发调试 Navlang 的,我们可以认为它大概率不会跑在性能敏感的环境里。

    另外,对我厂的 Navigator 产品而言,这样的方案还会让 GC 变得比较难执行。不过一样从 便利开发 的角度出发,我觉得性价比完全 OK。

    5. 总结

    这个需求比较特殊,涉及到 JS 函数的很多性质,包括运行时优化的知识,还是蛮值得大家琢磨的。

    另外就是所谓“尽信书不如无书”。eval() 的确存在一些问题,不能轻易使用,但是当需求摆在面前,经过充分的分析确认之后,我们该用还是要用。

  • Hello,2021

    Hello,2021

    2020 可能是史无前例的不受欢迎的一年,无论是微博、朋友圈、Twitter,极少看到有人怀念它的。我不禁为 2020 感到惋惜,明明不是它的罪过。

    不过我还是要说,在我经历过的 36 个年头里,2020 年是体感速度最快的一年,我现在都还能回忆起年初的种种,好像一睁眼一闭眼,就 2021 了,还有很多想做的事情没做……好吧,还是总结一下吧。

    回顾 2020

    先说身体。去年 4 月 28 日,早上起床突然发现脚踝肿了,怀疑是痛风,于是去医院看诊。医生觉得不太像是痛风,还是扭伤的可能性大。但是检查结果血糖没控制好,骨外科的大夫说糖尿病患者可能会出现随机的关节痛。哎,果然还是走上了糖尿病患者的老路。

    跟老婆商量了一下,正好疫情逐步得到控制,健身房也即将开放,于是我们办了健身卡开始健身。我也找医生换了新药控制食欲。开始效果很明显,食欲下降明显,几乎吃两口就饱了。体重下降很快,最低到 105+kg。可能是太过冒进,身体也出现一些不适,于是 8 月份去住了一周院调整。后来减了一些运动量,加了一些食量,目前感觉好多了,体重稳定在 106~108之间。

    开始锻炼之后,心肺功能和肌肉都得到了不小的加强,基本学会自由泳。

    总结一下:

    1. 终于有一年体重达标了,可喜可贺!
    2. 利拉鲁肽对我非常有效,不过半年过去,药效已经明显下降。
    3. 低血糖不好,控糖的目的一是血糖稳定,二是稳定在合适的范围内。
    4. 无氧运动加强肌肉,提升基础代谢;有氧运动减重

    side project 部分就比较惨,书没进展,肉大师没捡起来,新接的视频也没录完……春节前一定要录完,加油。

    博客大概写了 69 篇,还行吧,草稿箱里还有几篇没写完。访问量相比去年提升很多,坚持还是有用的。今年继续努力。

    旅游彻底黄了,一年没出门。哦,不对,去了几次深圳……

    终于下定决心把车换了,老车送回北京给表姐开,换了一辆亚洲龙(Avalon)代步。去年告别了很多人,包括 Avalon JS 框架的作者。新车挺好的,未来一时半会儿不会再换了。

    NAS 的机器弄出来了,没顾上搞系统,今年继续吧。

    2021 计划

    1. 坚持锻炼,把体重降到 105 kg
    2. 去年捡起来游泳,今年希望捡起来篮球
    3. 复活肉大师,把书写完
    4. 录完欠(签)下的教学视频
    5. 把前面设想的教学视频小语言做出来,然后再录 1~2 套视频
    6. 努力写博客,去年大概挣了 $20,希望今年够得上提款
    7. 旅游方面,如果国境线如果能开放,希望可以出去一次
    8. 感谢老板支持,去年年底终于把 Navlang + 扩展发出去了。那么今年要再努力一把:
      1. 上线 Chrome Web Store 和其它浏览器应用商店
      2. 能够收获 100+ 用户
      3. 完成付费使用功能
    9. 搞定一个 Side Project

    去年身体状况从五月折腾到八月,期间想了很多,觉得还是要拼一拼,主要是不拼也未必能好。所以今年还要多努力!加油!

  • 使用 Proxy 添加魔术属性/方法

    使用 Proxy 添加魔术属性/方法

    最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

    对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

    好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

    Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

    Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

    const obj = { name: 'meathill' };
    new Proxy(obj, {
      get(target, property) {
        // 如果对象中有要求的属性或方法,则返回
        if (property in target) {
          return target[property];
        }
        // 没有的话,进行其它处理
        return 'hello';
      }
    });
    
    obj.name // 'meathill'
    obj.age // 'hello'
    obj.sex // 'hello'

    接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

    如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


    关于如何创建具有魔法属性/方法的类,请移步阅读 使用 Proxy 创建有魔术属性/方法的类


    Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

    参考

    • 阮一峰的 ES6
    • 《深入理解 ES6》

  • Welcome to Navlang

    Welcome to Navlang

    过去两天,我厂在杭州召开了 OpenResty Con 2018,很多 OpenResty 的社区的小伙伴分享了很多使用 OpenResty 的心得。春哥也分享了 OpenResty(包括商业公司 OpenResty Inc)接下来的计划和未来的方向。说实话我已经很长时间不参会也不关心会了,因为很多会营养太少,商业太过。不过我不得不说,我厂的会干货真的多。

    回到主题,我也趁机搞了一个闪电演讲,介绍我厂的小语言之一——Navlang。遗憾的是,准备的不算充分,时间也比较短,所以介绍的不够清楚,导致好几个同学来问细节。

    既然已经公开我以后也会分享一些实现细节和心得。感兴趣的同学可以先看 Slide:

    开发这个语言的时候,我不太适应 Perl,也不太适应在春哥的要求下写代码,所以表现并不理想。如果有机会我还是希望能继续贡献 feature。

    另外,受到 Navlang 的启发,我觉得可以搞一个 TechLang,用编程的方式录制视频教程。将来有机会搞一下。