作者:曹阳、王歆宇
在网易内部,很多部门都是以NEJ+Regular作为基础库来进行前端开发,因此,各个部门也基于此积累了很多库,组件。这也产生了一个问题,一个新的项目如果启动,想去使用以前的积累,就必须和以前采用相同的技术方案以及选型,那么,有没有一种方法,在新项目(业务)启动时,既能兼容以往部门所沉淀的技术组件,又能够使用新技术,新方案给开发带来全新的感受,我们在【卡搭校园】这个产品做了以下的尝试。
在网易内部,大家一定对NEJ不陌生,早期乃至现在很多业务都基于NEJ框架进行开发,NEJ也包揽了模块依赖、umi调度系统、platform适配、构建打包等多个功能,可以说涵盖了从开发到构建上线的所有生命周期,足见之强大。可也正因为此,NEJ也显示的很重,无法像webpack,postcss,babel等通过插件/loader,来进行二次开发,有时候,一些新的想法和点子只能通过小聪明去解决。基于这样的考虑,我们首先做了一件事,解耦NEJ,即NEJ代码不需要通过自有的打包构建工具,而能被任意第三方框架/仓库使用。
众所周知,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模块相比,又更具灵活性:
如果能够找出NEJ的私有模块依赖系统与标准的模块系统之间的不同之处,将NEJ的私有模块依赖系统转化为标准的AMD/CMD/CommonJS/ES6 module系统,是不是也就意味着NEJ模块可以被其它类型的模块所用?
基于此,我们决定采取编写Babel插件的方式来进行NEJ module -> CommonJS module的转化。
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。
除了以上三点,babel转化NEJ的过程中,我们还需要解决如下几个问题:
在不断的分析NEJ源码以及尝试下,我们小组完成了babel插件babel-plugin-transform-nej-module
来做这件事情。
对一个常见的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);
插件的运行流程图及每一步的解释如下:
从文件的根开始:为什么要从根开始?在babel插件的不同的visitor中,难免会需要共享变量。这些共享变量的初始化,应该在每个文件第一次进入时进行。因此选择从根开始处理文件,以便在文件开始运行前做一些初始化处理。(Babel插件处理文件的顺序是并行的还是串行的,这一点尚有待验证。如果是串行的,在pre阶段进行初始化更好。)
取第一层节点:JS文件被Babylon parse成一棵AST树,取该树的第一层节点开始后续处理。
为什么取第一层?
NEJ.define
/define
语句中,形成模块。 如果NEJ模块被包含在其他代码块中,那它不一定能被执行到,则该模块是无效的。 即使一定能执行到,在NEJ打包时,也不会将模块外部的代码打包。因此,我们只考虑一个文件就是一个nej模块的情况
判断NEJ模块:nej.define()
或者 define()
函数被识别为NEJ模块
获取依赖列表和回调函数:处理了无依赖列表、依赖列表为空、回调函数为变量的情况。
进一步寻找回调函数(当回调函数为变量的时候):访问模块内代码的所有赋值语句,找到对回调函数变量赋值的语句。
将回调函数命名为nejModule:这一步很重要。用来定位回调函数的return和输出结果集空间的变动,并进行跟踪修改。
处理return:回调函数对应的return xxx
为module.exports = xxx; return;
处理依赖:
处理输出结果集空间(设为pro):将所有使用到pro
的代码替换为exports
,(不直接module.exports = pro,因为需要处理循环依赖)
保证模块内this指向window:使用自执行函数将全部语句包装起来。
text!
方式引入的css文件为空字符串,实际应该是css文件的内容;{platform}/element.js
被转化为./platform/element.js
,实际应当同时引入./platform/element.patch.js
,并提供平台设置参数进行适配如果你发现了任何其它异常情况,请一定要提出issue或直接联系我们,我们将会在最快时间内解决
NEJ模块转化为标准Common JS模块后,还需要再两项额外配置,来保证与webpack的结合使用:
将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')
}
}
...
}
在插件中配置非前缀的路径参数、以及严格模式的去除:
"plugins": [
...
"transform-remove-strict-mode",
[
"transform-nej-module",
{
"mode": "web"
}
],
///
],
说完如何处理NEJ,现在我们来分析一下,如何处理部门所沉淀的regular组件库?
我们所在的部门从16年起,开始搭建了一套用regular编写的组件库,大大小小封装了上百个模块及组件,为各条产品线提供支持。
众所周知,vue template和regular template是不同DSL语法的模板技术,两个框架拥有各自的语法, 在框架层面,如何能最小程度的兼容regular组件成了我们需要优先考虑的问题。
在regularjs中,组件被拆分为了 模板template + 数据data + 业务逻辑(实例函数)的组合。也就是说,只要满足上述三个条件,就可以实例出一个regular组件。
regular组件有两种使用方式,一种是直接实例化,一种是标签式。
1.对于直接使用new方法实例出来的组件,将所需要数据传入即可,注意要及时销毁。
2.对于标签式组件,就必须要有一个地方来承载regular模版,于是想到可以通过注册一个通用的vue组件,拿到regularjs模版,传入数据与逻辑,在该组件中进行实例化,从而达到承载regular组件的效果。
我们的目的就是设计并实现这样一个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组件中,拿到
this.$slots
模版this.rdata
数据this.revent
业务逻辑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
本文来自网易实践者社区,经田翔授权发布。