fis2项目迁移fis3的体验

刚接触fis2的时候,就已经出fis3了,不过项目都是用fis2做的,也不熟悉,也就没想过迁移的事。 趁着空闲,刷了下fis2的源码,再学习一下fis3,开始针对着项目打算做一下fis3的迁移尝试。

由于项目通过fis2,前后端指定了一定的规则以及代码目录规范,这次迁移尝试也就是以fis2的配置来做fis3的配置。

迁移前先系统了解一遍项目的fis2应用情况:

项目前端代码:主要有js、image、css、template

其中template由后端部署,相关资源后端通过读取map.json将模板所需依赖加载并渲染出实际页面

目录结构:
common: 公共部分,主要包括页面基本元素,如布局外壳模板(layout)、公共组件、库文件等
home: 业务部分,主要包括 业务页面模板(page)、页面组件(widget)等

再细化则是:

common

-page 放置layout模板
-static 放置公共静态资源内容,包括库、以及mod.js
-test-data
-widget 组件目录
-fis-conf.js 配置文件

home

-page 放置页面业务模板
-static 共用静态资源
-test-data
-test-interface
-widget 模块组件目录
-fis-conf.js 配置文件
-server.json 模板与url映射配置(本地开发用 )

先来看看common的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
fis.config.merge({
    namespace : 'common',
    // 插件配置节点
    modules : {
        // 编译器插件配置节点
        preprocessor : {
            ftl : 'extlang'
        },
        postprocessor : {
            ftl : 'require-async',
            js : 'jswrapper, require-async'
        },
        spriter : 'csssprites'
    },
    roadmap : {
        // 配置所有资源的domain
        domain : {
                'widget/map/**': 'http://xxx',
                '**': 'http://yyy'
                },
        path : [
            {
                // 定义图片不加md5戳
                reg : /(.*\-nohash\.(png|jpg|jpeg|gif))$/i,
                // 不使用md5文件名
                useHash : false,
                release : '/static/${namespace}/$&'
            },
            {
                // 实际部署不需要的文件:txt文件不处理\md文件
                reg : /(.*\.(txt|md))$/i,
                release : false
            },
            {
                // map.json文件
                reg : '**map.json', 
                // release : '/WEB-INF/classes/config/$&'
                release : '/WEB-INF/classes/config/$&'
            },
            {
                // data下的json文件
                reg : /^\/(data\/.*\.json)/i,
                // 发布到/template/widget/xxx目录
                release : '/WEB-INF/classes/$&'
            },
            {
                // widget下的tpl文件
                reg : /^\/(widget\/.*\.ftl)/i,
                // 是组件化的
                isMod : true,
                isHtmlLike : true,
                // 模板资源路径在widget/xxx
                url : '${namespace}/$1',
                // 发布到/template/widget/xxx目录
                release : '/template/${namespace}/$1'
            },
            {
                // widget下的js文件
                reg : /^\/(widget\/.*\.js)/i,
                // 是组件化的
                // 组件化的js文件会经过fis-postprocessor-jswrapper插件的define包装
                isMod : true,
                // 发布到/static/widget/xxx.js
                release : '/static/${namespace}/$1'
            },
            {
                // widget下的js文件
                reg : /^\/(static\/js\/.*\.js)/i,
                // 是组件化的
                // 组件化的js文件会经过fis-postprocessor-jswrapper插件的define包装
                isMod : true,
                //发布到/static/widget/xxx.js
                release : '/static/${namespace}/$1'
            },
            {
                // static下的js文件
                reg : /^\/(static\/lib\/.*\.js)/i,
                // 发布到/static/lib/xxx.js
                release : '/static/${namespace}/$1'
            },
            {
                reg : /^\/page\/(.+\.ftl)$/i,
                isMod : true,
                isHtmlLike : true,
                release : '/template/${namespace}/page/$1',
                extras : {
                    isPage : true
                }
            },
            {
                // 其他tpl文件
                reg : '**.ftl',
                isHtmlLike : true,
                // 发布到/template/目录下
                release : '/template$&'
            }, {
                // .sh文件不需产出
                reg : /^.*\.sh$/i,
                // 编译的时候不需产出
                release : false
            },
            {
                // .bat文件不需产出
                reg : /^.*\.bat$/i,
                // 编译的时候不需产出
                release : false
            },
            {
                reg : '**.ico',
                useHash : false,
                release : '/static$&'
            },
            {
                reg : /^.+$/,
                release : '/static/${namespace}$&'
            }
        ]
    },
    pack : {
        /* css部分 */
        'static/pkg/common-aio0.css' : [
            'static/css/common.css'
        ],
        'static/pkg/common-aio1.css' : [
            'widget/nav/nav.css',
            'widget/footer/footer.css'
        ],
        'static/pkg/layout-aio0.css' : [
            'static/css/layout.css'
        ],
        /* js部分 */
        'static/pkg/common-aio0.js' : [
            'static/mod.js',
            'static/js/common.js',
            'static/js/util.js'
        ],
        'static/pkg/common-aio1.js' : [
            'widget/nav/nav.js'
        ],

        // 以下是单独组件的js
         'static/pkg/flexslider-aio0.css' : [
            'widget/flexslider/flexslider.css'
        ],
        'static/pkg/flexslider-aio0.js' : [
            'widget/flexslider/flexslider.js'
        ]
    },
    deploy : {
        local : {
            // from参数省略,表示从发布后的根目录开始上传
            // 发布到当前项目的上一级的output目录中
            to : '../webapp'
        }
    },
    settings : {
        postprocessor : {
            jswrapper : {
                type : 'amd'
            }
        },
        spriter : {
            csssprites : {
                layout : 'linear'
            }
        },
        template : {
            left_delimiter : '<@',
            right_delimiter : '>'
        }
    }
});

配置文件注释相对全面,不多做描述,且来看看在fis3对应的该怎么配置?

首先要知道fis3跟fis2的一个大致区别,这点官方有说明。 -»戳我«-

先从简单入手,即项目本地开发模式下的配置,大概可以分为四类产出模式:

本地开发模式:dev
打包不压缩模式: 打包合并资源
打包压缩不加域名模式:用于本地运行接近线上的前端环境
生产模式:即打包压缩并打上域名的资源引用,这是部署线上环境的产出

项目的本地开发模式主要做了哪些工作?

解析资源依赖(包括模板的资源依赖、js的资源依赖、js同步依赖) 注: 异步依赖暂时不涉及
资源产出部署 (包括资源产出部署以及模板产出、依赖资源配置文件产出、过滤文件产出)

于是,照着fis3的思路,把fis2的配置对应的搬迁过来。

主要关注以下几点:

  1. 资源同名依赖: fis2默认解析模板与js文件的时候会自动同名依赖(查找是否有同名的js/css),fis3则需要主动配置useSameNameRequire: true
  2. 模板ftl的解析资源依赖:需要用到插件fis3-preprocessor-extlang, 原本插件并没有支持ftl文件,但是,直接把smarty的模板解析部分代码复制并把对应的tpl改成ftl,并加上自定义标签的起始标识符即可.
  3. amd包装: fis2项目结合了mod.js做了amd处理,但fis3的话,需要安装插件来做fis3-hook-commonjs

配置方式:

//amd模块支持
fis.hook('commonjs',{
    extList: ['.js']
});

以上三点关注到,也就差不多了,但在验证过程中还是出现了些问题:

1.js文件里的代码require('common:widget/a/a.js'), 产出时会被产出成require('common:widget/a/a'), 定位问题发现,原来是fis3默认解析依赖后貌似还原内容时用了file对象的moduleId来替换了,而moduleId默认是不带后缀的,源码那里有做处理。于是在配置匹配js时做了moduleId的定制处理:

//moduleId设置,为了兼容fis2项目的id命名规则
fis.match(/^\/(widget\/.*\.js)/i, {
    isMod: true,
    moduleId: '${namespace}:$1',
    release: "/static/${namespace}/$1"
})

【这个moduleId是个坑】原本以为配置widget下的js匹配moduleId设置就万事大吉了,然而,发现hook-commonjs对js的处理中,当js内有require其他js的情况下,也是会出现require不带后缀的情况,看了下源码,hook-commonjs中对lookup事件时触发的处理,lookup.js有处代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 跨模块引用 js
  if (info.isFISID && !info.file && (/\.js$/.test(info.rest) || !/\.\w+$/.test(info.rest))) {
    info.id = info.id || info.rest;
    var mod = getJsExtensionMoited();

    // 只有在省略后缀的模式下才启用。
    if (mod && !/^(https?\:)?\/\//.test(info.id)) {
      info.moduleId = info.id.replace(/\.js$/, '');
      info.id = info.moduleId + '.js';
    }
  }

其中调用了getJsExtensionMoited方法:

1
2
3
4
5
6
7
8
function getJsExtensionMoited() {
  if (typeof moitJsExtension === 'undefined') {
    var testFile = fis.file(fis.project.getProjectPath() + '/test.js');
    // console.log(testFile);
    moitJsExtension = testFile.id !== testFile.moduleId;
  }
  return moitJsExtension;
}

可以看出只要这个方法返回false就可以规避js资源moduleId的干涉了,然而这里并不是;
从实现上看,这里的处理:通过建立临时的js文件,然后通过按照配置来查看这个文件的file对象的id跟moduleId是否不同。由于不是在widget下生成的js,所以得出的moduleId依然是不带后缀的,怎么办? 给全部的js都加上moduleId的定制啦。

fis.match(/^\/(.*\.js)/i,{
    useSameNameRequire: true,
    moduleId: '${namespace}:$1'
})

2.extlang的处理:关于extlang的处理,遇到的问题是,如果ftl里面有js代码 ,并且有require其他js的时候,fis产出并没有为这个模板添加关于这个被require的js文件依赖,咋一看,被require的js,好像在第一点里说的是同一个原理 ,那应该是hook-commonjs该做的模块化识别(但这个插件是针对js的);
然后发现代码关于ftl里的js代码处理处,注释了一段代码(应该属于原先的脚本处理)

1
2
3
4
5
6
7
8
9
    if (comment) {
      m = fis.compile.analyseComment(comment);
    } else if (script) {
      m = fis.compile.xLang(script, jscode, file, 'js');
     //m = script + fis.compile.extJs(jscode, null, file);
    } else if (style) {
      m = fis.compile.xLang(style, csscode, file, 'css');
      // m = style + fis.compile.extCss(csscode, null, file);
    }

于是,把现有的注释掉 ,把注释的释放出来 ,解决了。真是奇怪- -,源码也没标明啥回事,但都是用的fis3的compile.js的方法,回头去看一下。(如果不这么做的话 ,也可以是在js里加入<!-- @require xxx -->这类的主动注释 ,extlang的主要识别功能, 但这样迁移的成本又增加了)

–update–

通过走读compile.js的代码,发现不用释放方法,而是通过设置 ftl 匹配规则时添加 pipeEmbed:false也可以识别到依赖了。

fis.match('**.ftl', {
        isHtmlLike: true,
        useSameNameRequire: true,
        pipeEmbed:false,
        preprocessor: fis.plugin('extlang'),
        release: "/template/$&"
    })

这里说到的非内置插件,可以通过放置到项目目录的node_module以供使用,不再需要像fis2那样设置全局安装了。

update 2016-12-14

由于fis3升级后,修改了一些策略,导致不能读取到项目目录的node_module了。

修改 fis.require 策略,不再名字优先,而是路径优先。且不会找到项目以外的 npm 包。

但通过issue上的交流反馈,很快就得到解决方案,那就是在配置文件加入代码配置:

var path = require('path');

fis.require.paths.push(path.join(__dirname, '../node_modules'));
//fis.require.paths.push(path.join(path.dirname(__dirname), 'node_modules'));

到这里,开发模式基本就这么回事了,接下来就是压缩、打包的问题了。

在fis2里,打包压缩、加域名是通过指令参数来完成的 ,但是fis3的话,通过fis.media()设置标识,在指令运用时,输入fis3 release mediaName 即可。

关于压缩:

1.压缩js

//压缩js
fis.media('prd').match('*.js',{
    useHash:true,
    useSameNameRequire: true,
    optimizer:fis.plugin('uglify-js')
});

如上代码,通过设置 optimizer来设置js的压缩插件, 此插件为fis3内置,可以直接使用。

2.压缩css

//压缩css
fis.match('*.css',{
    useHash:true,
    optimizer: fis.plugin('clean-css'),
    domain: domain
});

同理,css文件也是通过optimizer设置,使用的是clean-css内置插件。

3.图片优化

1
2
3
fis.media('pro').match('*.png',{
     optimizer: fis.plugin('png-compressor')
})

png的图片可以用png-compressor来做优化处理(实际上这块没怎么了解,只是在做生成map.json文件对比的时候发现fis2跟fis3的产出有所不同,才留意到。)

关于打包:

打包这一块,fis2里是直接定义pack配置对象,而fis3可以设定匹配文件后packTo为某个文件。如下格式:

1
2
3
4
fis.media('pro').match('*.js',{
            packOrder:1,
            packTo : 'all.js'
         });

这样 所有js都会合并才成all.js

但是!!! 项目里一般都是几个业务组件合并到一块去,所以当业务页面多起来的时候 ,要合并的配置就多了。

为了迁移更快(其实是懒),我将fis2的pack对象数据直接独立成一个set-pack.json文件(缺点就是不能加注释了,不过可以通过使用json-comments来解决这个缺点。)

然后在配置文件里读取这个json解析出配置对象,再通过遍历对象组合成多个fis.match .

1
2
3
4
5
6
7
8
9
10
//打包配置: packOrder 值越小,越先加入
for(var packPath in pack){
    var filesPath = pack[packPath];
    filesPath.forEach(function(e, i){
        fis.media('pro').match(e,{
            packOrder:i,
            packTo : packPath
         });
    });
}

现在想到的还有一个缺点就是,fis.match可以写成链式调用的 ,但是这里就被我断开了,不够优雅。
在做打包的时候发现打包顺序问题,通过设置packOrder值即可控制打包顺序。

– update –

关于链式处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var FilePack = function(pack){
    var _this = this;
    for(var packPath in pack){
        var filesPath = pack[packPath];
        filesPath.forEach(function(e, i){
            _this.match(e,{
                packOrder:i,
                packTo : packPath
             });
        });
    }
    return this;
};
fis.media('prd').FilePack = fis.media('pro').FilePack = FilePack;

通过定义方法,并赋值给fis实例,方法内部实现链式返回即可。不过需要注意的是,必须赋值给对应media的fis实例才会生效,否则将找不到方法。

其他

文件产出部署,对于本地项目一般都是输出到common 和home同级目录下的webapp目录里,命令的做法是 -d ../webapp
不过fis3自带deploy-local-deliver插件,可以这样做

1
2
3
4
5
6
7
//所有文件都部署到
fis.match(/^.+$/, {
    release: "/static/${namespace}/$&",
    deploy: fis.plugin('local-deliver', {
        to: '../webapp'
    })
})

项目打包压缩的时候通常做的是生成md5命名的模式,这里可以通过设置useHash:true来做, 域名则使用domain配置。

关于css sprite设置,通过设置spriter来做,由于fis3默认是不产出map.json依赖表的,可以做的方式有两种,一种是定义一个json文件,文件内容为:__RESOURCE_MAP__ ,fis3会识别文件的这个标识符输出依赖表。

有一种更优雅的,我在fis3-jello上看到的。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fis.match('::package', {
      postpackager: function(ret) {
        var path = require('path')
        var root = fis.project.getProjectPath();
        var ns = fis.get('namespace');
        var mapFile = ns ? (ns + '-map.json') : 'map.json';
        var map = fis.file.wrap(path.join(root, mapFile));
        map.setContent(JSON.stringify(ret.map, null, map.optimizer ? null : 4));
        ret.pkg[map.subpath] = map;
      },
      spriter: fis.plugin('csssprites',{
         layout : 'linear',
         margin: 50
      })
}).match('**map.json', {
    release: "/WEB-INF/classes/config/$&"
});

暂时就这一些,其他的问题,再后续做补充。

附上,整理后的fis3配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
开发模式:default

本地debug(打包不压缩)模式:ldb [local-debug]

生产不加域名模式(打包压缩):prd [product-debug]

生产模式:pro
*/
//读取打包文件配置
var pack = require('./set-pack.json').pack;

var path = require('path');
fis.require.paths.push(path.join(__dirname, '../node_modules'));
//fis.require.paths.push(path.join(path.dirname(__dirname), 'node_modules'));

var FilePack = function(pack){
    var _this = this;
    for(var packPath in pack){
        var filesPath = pack[packPath];
        filesPath.forEach(function(e, i){
            _this.match(e,{
                packOrder:i,
                packTo : packPath
             });
        });
    }
    return this;
};
fis.media('prd').FilePack = fis.media('pro').FilePack = FilePack;

//设置路径前缀
var domain = 'http://xxx';
var libDomain = '';

//设置命名空间
fis.set('namespace','common');

//amd模块支持
fis.hook('commonjs',{
    extList: ['.js']
});

//所有文件都部署到
fis.match(/^.+$/, {
    release: "/static/${namespace}/$&",
    deploy: fis.plugin('local-deliver', {
        to: '../webapp'
    })
})
//模板文件设置:同名加载、识别require
.match( '**.ftl', {
    isHtmlLike: true,
     useSameNameRequire: true,
     pipeEmbed:false,
     preprocessor: fis.plugin('extlang'),
    release: "/template/$&"
})
//页面输出模拟数据存放点
.match(/^\/(data\/.*\.json)/i, {
    release: "/WEB-INF/classes/$1"
})
.match(/^\/(widget\/.*\.ftl)/i, {
    isMod: true,
    isHtmlLike: true,
    preprocessor: fis.plugin('extlang'),
    url: "${namespace}/$1",
    release: "/template/${namespace}/$1"
})
.match(/^\/(.*\.js)/i,{
    useSameNameRequire: true,
    moduleId: '${namespace}:$1'
})
//moduleId设置,为了兼容fis2项目的id命名规则
.match(/^\/(widget\/.*\.js)/i, {
    isMod: true,
    moduleId: '${namespace}:$1',
    release: "/static/${namespace}/$1"
})
.match(/^\/(static\/js\/.*\.js)/i, {
    isMod: true,
    moduleId: '${namespace}:$1',
    release: "/static/${namespace}/$1"
})
//最高优先级处理引用库文件
.match(/^\/(static\/lib\/.*)/i, {
    useHash: false,
    query: "?_v=20141017",
    release: "/static/${namespace}/$1"
},true)
.match(/^\/page\/(.+\.ftl)$/i, {
    isMod: true,
    isHtmlLike: true,
    preprocessor: fis.plugin('extlang'),
    release: "/template/${namespace}/page/$1",
    extras: {
        "isPage": true
    }
})
.match( '**.ico', {
    useHash: false,
    release: "/static/$1"
})
.match(/^.*\.bat$/i, {
    release: false
})
.match('::package', {
      postpackager: function(ret) {
        var path = require('path')
        var root = fis.project.getProjectPath();
        var ns = fis.get('namespace');
        var mapFile = ns ? (ns + '-map.json') : 'map.json';
        var map = fis.file.wrap(path.join(root, mapFile));
        map.setContent(JSON.stringify(ret.map, null, map.optimizer ? null : 4));
        ret.pkg[map.subpath] = map;
      },
      spriter: fis.plugin('csssprites',{
         layout : 'linear',
         margin: 50
      })
}).match('**map.json', {
    release: "/WEB-INF/classes/config/$&"
});

/**    [打包压缩不加域名]prd模式处理 只有 fis3 release prd 时才会执行  **/
//png图片压缩
fis.media('prd').match('*.png',{
     optimizer: fis.plugin('png-compressor')
})
//useHash加md5
.match('*.{png,jpg}',{
    useHash:true
})
//压缩css
.match('*.css',{
    useHash:true,
    optimizer: fis.plugin('clean-css'),
    useSprite: true
})
.FilePack(pack)
.match('*.js',{
    useHash:true,
    useSameNameRequire: true,
    optimizer:fis.plugin('uglify-js')
});

/**    pro模式处理 只有 fis3 release pro 时才会执行  **/
//png图片压缩
fis.media('pro').match('*.png',{
     optimizer: fis.plugin('png-compressor')
})
//useHash加md5
.match('*.{png,jpg}',{
    useHash:true,
    domain: domain
})
//压缩css
.match('*.css',{
    useHash:true,
    optimizer: fis.plugin('clean-css'),
    domain: domain
})
.FilePack(pack)
.match('*.js',{
    useHash:true,
    useSameNameRequire: true,
    optimizer:fis.plugin('uglify-js'),
    domain: domain
})
.match('widget/map/**,static/lib/tiny_mce/**',{
    domain:libDomain
});