VUE & Regular & NEJ 无痛融合解决方案(上篇)

猪小花1号2018-08-31 13:34

作者:曹阳、王歆宇


0 前言

在网易内部,很多部门都是以NEJ+Regular作为基础库来进行前端开发,因此,各个部门也基于此积累了很多库,组件。这也产生了一个问题,一个新的项目如果启动,想去使用以前的积累,就必须和以前采用相同的技术方案以及选型,那么,有没有一种方法,在新项目(业务)启动时,既能兼容以往部门所沉淀的技术组件,又能够使用新技术,新方案给开发带来全新的感受,我们在【卡搭校园】这个产品做了以下的尝试。

1 NEJ篇

在网易内部,大家一定对NEJ不陌生,早期乃至现在很多业务都基于NEJ框架进行开发,NEJ也包揽了模块依赖umi调度系统platform适配构建打包等多个功能,可以说涵盖了从开发到构建上线的所有生命周期,足见之强大。可也正因为此,NEJ也显示的很重,无法像webpack,postcssbabel等通过插件/loader,来进行二次开发,有时候,一些新的想法和点子只能通过小聪明去解决。基于这样的考虑,我们首先做了一件事,解耦NEJ,即NEJ代码不需要通过自有的打包构建工具,而能被任意第三方框架/仓库使用。

1.1 模块依赖

众所周知,NEJ有属于自己的模块依赖系统,即NEJ.define(),一个标准的NEJ模块系统会有这样的代码

NEJ.define( [
    'pro/{mode}/base'
    './component.js',
    'text!./web/component.html',
    'css!./web/component.css'
],function(Base,Button,html,css,p,o,f,r){
    // ...具体逻辑
})

显然,NEJ的模块依赖系统属于AMD类型,但是其语法与标准的AMD模块相比,又更具灵活性:

  1. 文件依赖url支持参数,可通过define.js配置:pro/{mode}/base
  2. 支持text,css,regular等拓展关键字来引入不同类型的文件:text!./web/component.html
  3. 模块中会自动注入4个参数:p,输出结果集空间;o,一个空对象;f,一个return false的函数;r,一个空数组;

1.2 模块转化:babel插件

如果能够找出NEJ的私有模块依赖系统与标准的模块系统之间的不同之处,将NEJ的私有模块依赖系统转化为标准的AMD/CMD/CommonJS/ES6 module系统,是不是也就意味着NEJ模块可以被其它类型的模块所用?

基于此,我们决定采取编写Babel插件的方式来进行NEJ module -> CommonJS module的转化。

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

1.2.1 使用babel转化NEJ需要解决的问题


除了以上三点,babel转化NEJ的过程中,我们还需要解决如下几个问题:


  1. NEJ return && 输出结果集空间的处理
  2. 依赖系统中循环依赖的处理
  3. NEJ依赖中,默认this = window
  4. NEJ很多文件在严格模式下会报错,因为存在很多非严格写法,(如 xxx.bind(undefine),非严格模式情况下, this指向window;而严格模式下this指向undefined)


在不断的分析NEJ源码以及尝试下,我们小组完成了babel插件babel-plugin-transform-nej-module来做这件事情。


1.2.2 插件效果示例


对一个常见的NEJ模块文件:


// NEJ模块
NEJ.define( [
    '../component.js',
    'text!./component.html',
    'text!./component.css'
],function(
    Component,
    html,
    css,
    pro,
    o, f, r
) {
    return uxModal;
});


babel-plugin-transform-nej-module会将其转化为:


// CommonJS模块
(function nejModule() {
    var Component = require('../component');
    var html = require('./component.html');
    var css = require('./component.css');

    var css = "";

    var pro = exports;

    var o = {};
    var f = function () {};
    var r = [];

    module.exports = uxModal;
    return;
}).call(window);


查看更多示例


1.2.3 插件运行剖析

插件的运行流程图及每一步的解释如下:

  1. 从文件的根开始:为什么要从根开始?在babel插件的不同的visitor中,难免会需要共享变量。这些共享变量的初始化,应该在每个文件第一次进入时进行。因此选择从根开始处理文件,以便在文件开始运行前做一些初始化处理。(Babel插件处理文件的顺序是并行的还是串行的,这一点尚有待验证。如果是串行的,在pre阶段进行初始化更好。)

  2. 取第一层节点:JS文件被Babylon parse成一棵AST树,取该树的第一层节点开始后续处理。

为什么取第一层?

  • 在以NEJ为基础的前端工程中,所有的业务代码都包含在NEJ.define/define语句中,形成模块。 如果NEJ模块被包含在其他代码块中,那它不一定能被执行到,则该模块是无效的。 即使一定能执行到,在NEJ打包时,也不会将模块外部的代码打包。
  • 如果从任意一条语句开始,可能会遇到其他文件中的define函数被识别为nej模块情况。

因此,我们只考虑一个文件就是一个nej模块的情况

  1. 判断NEJ模块nej.define() 或者 define()函数被识别为NEJ模块

  2. 获取依赖列表和回调函数:处理了无依赖列表、依赖列表为空、回调函数为变量的情况。

  3. 进一步寻找回调函数(当回调函数为变量的时候):访问模块内代码的所有赋值语句,找到对回调函数变量赋值的语句。

  4. 将回调函数命名为nejModule:这一步很重要。用来定位回调函数的return和输出结果集空间的变动,并进行跟踪修改。

  5. 处理return:回调函数对应的return xxxmodule.exports = xxx; return;

  6. 处理依赖

  • 初始化注入参数;
  • 标准化依赖路径;
  • 初始化css文件为空字符串,以兼容NEJ对css的处理,尽管这些处理在不使用define函数的时候是无效的。
  1. 处理输出结果集空间(设为pro):将所有使用到pro的代码替换为exports,(不直接module.exports = pro,因为需要处理循环依赖)

  2. 保证模块内this指向window:使用自执行函数将全部语句包装起来。

1.2.4 方案不足之处

  • 尚存在两种罕见的异常未做处理:
  1. text!方式引入的css文件为空字符串,实际应该是css文件的内容;
  2. 输出结果集空间被直接修改为其它对象;
  • NEJ对于不同平台的适配处理:
  1. {platform}/element.js被转化为./platform/element.js,实际应当同时引入./platform/element.patch.js,并提供平台设置参数进行适配

如果你发现了任何其它异常情况,请一定要提出issue或直接联系我们,我们将会在最快时间内解决

1.3 与webpack的结合

NEJ模块转化为标准Common JS模块后,还需要再两项额外配置,来保证与webpack的结合使用:

  1. 将NEJ的路径参数配置为weppack别名,以下是已知的NEJ路径参数配置:

    module.exports = {
     ...
     resolve: {
         alias: {
             'base': resolve('lib/nej/src/base'),
             'lib': resolve('lib/nej/src'),
             'ui': resolve('lib/nej/src/ui'),
             'util': resolve('lib/nej/src/util')
         }
     }
     ...
    }
    
  2. 在插件中配置非前缀的路径参数、以及严格模式的去除:

 "plugins": [
    ...    
    "transform-remove-strict-mode",
    [
      "transform-nej-module",
      {
        "mode": "web"
      }
    ],
    ///
  ],

2 Regular篇

说完如何处理NEJ,现在我们来分析一下,如何处理部门所沉淀的regular组件库?

我们所在的部门从16年起,开始搭建了一套用regular编写的组件库,大大小小封装了上百个模块及组件,为各条产品线提供支持。

众所周知,vue template和regular template是不同DSL语法的模板技术,两个框架拥有各自的语法, 在框架层面,如何能最小程度的兼容regular组件成了我们需要优先考虑的问题。

2.1 原理探究

在regularjs中,组件被拆分为了 模板template + 数据data + 业务逻辑(实例函数)的组合。也就是说,只要满足上述三个条件,就可以实例出一个regular组件。

regular组件有两种使用方式,一种是直接实例化,一种是标签式。

1.对于直接使用new方法实例出来的组件,将所需要数据传入即可,注意要及时销毁。

2.对于标签式组件,就必须要有一个地方来承载regular模版,于是想到可以通过注册一个通用的vue组件,拿到regularjs模版,传入数据与逻辑,在该组件中进行实例化,从而达到承载regular组件的效果。

2.2 开始设计

我们的目的就是设计并实现这样一个RegularComponent(简称RC)组件。

首先遇到的问题是如何获取标签内的模版?

我们可以利用vue插槽

业务组件

// 业务组件模版
<rc ref="rc" :revent="REvent" :rdata="RData" :rfilter="RFilter">
    <ux-button value={name} on-click={this.clickBtn($event)}></ux-button>
</rc>

RC组件

// RC组件模版
<div ref="rc">
    <slot/>
</div>

这样就可以在RC组件中,拿到

  1. this.$slots 模版
  2. this.rdata 数据
  3. this.revent 业务逻辑
  4. this.rfilter 过滤器

这里需要注意,this.$slots 直接获取到的是vue规范的AST,下面称为VNode,我们可以封装一个方法将VNode转为模版字符串,再去执行实例化regular的行为。

let attrStringify = obj => {
    let ret = '';
    for (let i in obj) {
        if (obj[i]) {
            ret += ' ' + i + '="' + obj[i] + '"';
        } else {
            ret += ' ' + i;
        }
    }
    return ret;
};

// 拼装模版
let formatTpl = arr => {
    let tpl = '';
    arr.forEach(item => {
        if (item.text) {
            tpl += item.text;
        } else if (item.tag) {
            tpl += '<' + item.tag;

            // 组装attr
            if (item.data) {
                tpl += attrStringify(item.data.attrs) + '>';
            } else {
                tpl += '>';
            }

            // 组装子节点
            if (item.children) {
                tpl += formatTpl(item.children);
            }

            // 闭合组件标签
            tpl += '</' + item.tag + '>';
        } else if (item.children) {
            tpl += formatTpl(item.children);
        }
    });
    return tpl;
};

拿到这些数据,就可以生成reuglar组件了。

到了这一步,还没有完全实现我们想要的效果,因为vue插槽会将内容分发至子组件,查看源码可以看到模版代码直接展示在页面上;而且模版中的标签式组件是在regular中注册的,直接使用会报错,这是不希望看到的。

[Vue warn]: Unknown custom element: <ux-button> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

不过修复这个问题并不困难,只需隐藏这个插槽即可。

<div ref="rc">
    <slot v-if="false" />
</div>

现在组件实例化好了,假设需要异步获取数据,同步至regular组件,要如何更新呢?

在regular中,我们一般在接口请求回调中,将返回的数据赋值到this.data上,然后调用regular的update方法来触发一轮脏检查来同步数据。

在使用RC时,当vue组件中RData发生变化时,会自动同步至RC组件,但regular组件实例化完成之后,不会继续更新数据。我们只要在RC组件内部监听这个对象,再去更新regular组件内的数据即可,这个流程可以用下图表示:


相关阅读:VUE & Regular & NEJ 无痛融合解决方案(下篇)

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经田翔授权发布。