基于fis3的vue单文件组件方案

阿凡达2018-08-01 12:49

背景

自从上次迁移到vue之后(详见jQuery到Vue的迁移之路),还有个开发体验上的问题没有解决,就是没法用vue官方推荐的单文件组件的方式来开发组件。原因是官方推荐的构建工具为webpack,提供了专用的vue-loader去解析x.vue文件。而我们目前没法用的主要原因是(我自己觉得)在线上机器装一些其他npm包不太现实,会有其他影响,所以就没有去找相关的方案。知道前几天主管告诉我线上机器装插件没什么问题,想装直接联系相关同事就可以。于是,手又开始痒了,单文件组件的日子要来了。

相关插件

fis3-parser-babel

这个插件是fis3在编译单文件的parse阶段的babel插件,作用就是babel的作用咯,编译es6代码。

fis3-parser-vue-component

这个插件的作用跟webpack中的vue-loader的作用类似,就是将x.vue文件进行编译,产出js文件或者js+css文件。

fis3-hook-commmonjs

由于使用fis3-parser-babel插件之后,我们的es6的module语法会被编译成浏览器依旧无法解析的commonjs语法,因此需要这个插件来将commonjs所在的模块包装成AMD模块,再由下面会提到的mod.js来在浏览器中执行。

mod.js

这个插件就是为了提供AMD执行环境的一个精简版的模块加载器。

单文件编译过程

有了上面提到的插件,x.vue文件就可以通过fis3的构建过程被编译成能在浏览器中运行的代码了,具体流程如下:

使用方法

安装插件

node版本尽量6.x之上

安装fis3-parser-babel, fis3-parser-vue-component, fis3-hook-commonjs

npm install -g fis3-parser-babel
               fis3-parser-vue-component
               fis3-hook-commonjs

如果本地有安装 NVM 等 node版本管理工具,默认安装会在 nvm 的 node_modules 之下,可能会发生找不到 fis3-hook-commonjs 的问题,只需要将其复制到/usr/local/lib/node_modules/即可解决

引入mod.js(用来给将被babel转换成commonjs的es6模块代码包装成AMD的模块化代码提供执行环境)。可以放到本地来引用或者之后应该会传到nie线上直接引用线上地址。

// index.html
<script src="./mod.js"></script>

修改配置文件

<!--param start-->
//修改cdn的绝对路径(测试环境)
fis.set('cdn-path','$cdn-path$');
//修改cdn的绝对路径(正式环境)
fis.set('cdn-path-release','$cdn-path-release$');
//修改雪碧图放大缩小倍数,默认是1,iphone是0.5
fis.set('css-scale',1);
//修改include文件的域名
fis.set('include-host','http://qnm.163.com');
fis.set('livereload.hostname', '10.242.35.220');
<!--end-->


//配置通用
fis.set('project.files', ['src/**']);
fis.set('project.ignore', ['dist/**', 'release/**', 'README.md' , 'local/**' ,'.git/**', 'fis-conf.js']);
fis.set('charset', 'utf-8');
fis.set('project.charset', 'utf-8');

fis.match('**.less', {
    parser: fis.plugin('less'), // invoke `fis-parser-less`,
    rExt: '.css'
});
fis.match('**.tmpl', {
    parser: fis.plugin('bdtmpl'),// invoke `fis-parser-bdtmpl`
    isJsLike : true,
    release : false
},true);
fis.match('**.{html, vue:html}', {
    parser: fis.plugin('html-ejs'), // invoke `fis-parser-html-ejs`
    postprocessor : fis.plugin('include',{
        host : fis.get('include-host'),
        debug : true,
        release : false,
        encode : 'utf-8'
    }),
    useCache : false
});
fis.match('**.js', {
    parser: [
        fis.plugin('babel'),
    ]
});

fis.match(/^\/src\/(.*)$/i,{
    release : "$1",
    useCache : false
});
fis.match(/^\/src\/css\/_.*\.(css|less)/i,{
    release : false
});

fis.match(/^\/src\/.*\/(_.*)$/i,{
    release : "temp_file/$1"
});

fis.match(/^\/src\/css\/(.*\.png)$/i,{
    release : "img/spriter/$1"
});

fis.match(/^\/src\/data\/(.*)$/i,{
    useHash : false,
    useDomain : true,
    useSprite : true
},true);

fis.match('src/(**).vue', {
    isMod: true,
    moduleId: '$1',
    rExt: 'js',
    useSameNameRequire: true,
    parser: [
        fis.plugin('vue-component', {
            // vue@2.x runtimeOnly
            runtimeOnly: true,          // vue@2.x 有润timeOnly模式,为ture时,template会在构建时转为render方法

            // styleNameJoin
            styleNameJoin: '',          // 样式文件命名连接符 `component-xx-a.css`

            extractCSS: true,           // 是否将css生成新的文件, 如果为false, 则会内联到js中

            // css scoped
            cssScopedIdPrefix: '_v-',   // hash前缀:_v-23j232jj
            cssScopedHashType: 'sum',   // hash生成模式,num:使用`hash-sum`, md5: 使用`fis.util.md5`
            cssScopedHashLength: 8,     // hash 长度,cssScopedHashType为md5时有效

            cssScopedFlag: '__vuec__',  // 兼容旧的ccs scoped模式而存在,此例子会将组件中所有的`__vuec__`替换为 `scoped id`,不需要设为空
        })
    ],
    packTo: '/src/build/components.js'
});
fis.match('src/components/**.css', {
    packTo: '/src/build/components.css'
});
// vue组件中ES2015处理
fis.match('src/(**).vue:js', {
    parser: [
        fis.plugin('babel'),
    ]
});
fis.hook('commonjs', {
    // 配置项
});
//配置打本地包
//fis.hook('relative');
fis.media('local')
.match('**', {
    relative: true,
    charset : fis.get("charset"),
    deploy: fis.plugin('encoding')
})
.match("**.{html, vue:html}",{
    postprocessor : fis.plugin('include',{
        nouse : true
    })
})
.match('**', {
    deploy: fis.plugin('local-deliver', {
        to: './local'
    })
});

//配置测试打包

fis.media('dist')
.match('**.{js, vue:js}', {
    postprocessor : fis.plugin('replace',{
        debug : "dist"
    })
})
.match('*.{js,css,less,png,jpg,jpeg,gif,mp3,mp4,flv,swf,svg,eot,ttf,woff}',{
    domain: fis.get("cdn-path"),
    useHash: true
})
.match('src/build/*.{js,css}', {
    useHash: false
})
.match('::package', {
    spriter: fis.plugin('csssprites',{
        layout: 'matrix',
        margin: '0'
    }),
    postpackager : [fis.plugin('usemin'),fis.plugin('supply')]
})
.match('*.{css,less}',{
    useSprite : true
})
.match('**', {
    charset : fis.get("charset"),
    deploy: [fis.plugin('encoding'),fis.plugin('local-supply', {
        to: './dist',
        exclude : ['cms','inline','temp_file','config']
    })]
});

//配置正式打包

fis.media('release')
.match('**.{js, vue:js}',{
    postprocessor : fis.plugin('replace',{
        debug : "release"
    }),
    optimizer: fis.plugin('uglify-js',{
        output : {
            ascii_only : true
        }
    })
})
.match('**.html:js',{
    optimizer: fis.plugin('uglify-js')
})
.match('*.{css,less}',{
    optimizer: fis.plugin('clean-css')
})
.match('**html:css',{
    optimizer: fis.plugin('clean-css')
})
.match("**.{html, vue:html}",{
    postprocessor : fis.plugin('include',{
        nouse : true
    })
})
.match('*.{js,css,less,png,jpg,jpeg,gif,mp3,mp4,flv,swf,svg,eot,ttf,woff}',{
    domain: fis.get("cdn-path-release"),
    useHash: true
})
.match('src/build/*.{js,css}', {
    useHash: false
})
.match('::package', {
    spriter: fis.plugin('csssprites',{
        layout: 'matrix',
        margin: '0'
    }),
    postpackager : [fis.plugin('usemin'),fis.plugin('supply')]
})
.match('*.{css,less}',{
    useSprite : true
})
.match('**', {
    charset : fis.get("charset"),
    deploy: [fis.plugin('encoding'),fis.plugin('local-supply', {
        to: './release',
        exclude : ['cms','inline','temp_file','config']
    })]
});

代码组织

首先,在src文件夹下面新建一个build文件夹,在里面新建两个空文件components.csscomponents.js用来存放构建好的组件代码。

然后,在入口html文件中引入这两个文件:

// index.html
<link rel="stylesheet" href="./build/components.css">
...
<script src="./build/components.js"></script>

这样在fis就可以根据相对路径在构建时替换相应的线上url。(如果省略这一步的话,fis3在构建的时候会因为找不到这两个文件而跳过之后的替换资源url步骤)

模块化代码编写

完成上述步骤就可以开始写单文件组件了,比如src/components/下的test组件:

// src/components/test.vue
<template>
    <div class="test-component">
        <link rel="import" href="../xxx.html?__inline"> // link正常使用
        <test2></test2>
        <div class="test1"></div>
        <div class="test2"></div>
        <div class="test3"></div>
    </div>
</template>

<script>
    import test2 from 'components/test2';   // ES6模块语法
    export default {
        components: {
            'test2': test2  // 局部注册组件
        }
    }
</script>

<style lang="less" scoped>  // 同样支持less等预编译工具和scoped作用域限制
    .test-component {
        color: blue;
    }
    .test1 {
        background-image: url(../img/avatar-bg.png?__sprite);   // 雪碧图正常使用
    }
    .test2 {
        background-image: url(../img/award-bg.png?__sprite);
    }
    .test3 {
        background-image: url(../img/award-title-other.png?__sprite);
    }
</style>

另外,在src/xxx/app.js的应用入口处,还可以跟以前一样进行组件全局注册:

// 组件
import test from 'components/test';

// 注册全局组件
var components = {
    test
};
for (var key in components) {
    Vue.component(key, components[key]);    // 注册全局组件
}

// 初始化vue
new Vue({
    el: '#app',
    data: function() {
        return {
        }
    },
})

然后就可以在应用范围内使用全局注册的组件了:

// src/index.html
<body>
    <div id="app">
        <test></test>   // 被注册的组件
    </div>
    ...
</body>

需要注意的地方

组件存放的地方和引用组件的路径

上面的例子组件是存放在src/components/目录下的,我们在fis-conf.js文件里有一个配置:

fis.match('src/(**).vue', {
    ...
    moduleId: '$1',
    ...
});

这里的moduleId代表用mod.js包装的AMD模块的模块id,这里根据正则匹配将其设置成了components/文件名,如果不设置则模块id为资源的绝对路径,这里相当于简化了引用组件的方式:

// 引用test组件
import test from '/src/components/test.vue'; // 配置前
import test from 'components/test';          // 配置后

同理,如果还有容器组件,放在了src/containers/目录下,那么引用容器组件的时候就是:

// 引用容器组件
import xxx from '/src/containers/xxx.vue'; // 配置前
import xxx from 'containers/xxx';          // 配置后

另外,如果愿意的话,也可以修改配置进一步缩短模块id,比如只把文件名当做模块id也可以。

其他js文件中的es6模块的写法

根据mod.js的说法:

注意:需要对目标文件设置 isMod 属性,说明这些文件是模块化代码。

我们需要将模块化文件在编译的时候就指明。在目前的配置文件中,我们通过下面代码将xxx.vue单组件文件指定为模块文件:

fis.match('src/(**).vue', {
    isMod: true,
    ...
});

这样如果我们在xxx.vue文件中使用es6的module语法的话,首先会被babel转换成commonjs的语法,然后再通过fis-hook-commonjs插件配合mod.js实现在浏览器中的使用。

因此,如果我们需要在其他js文件中使用es6的module语法,需要在配置文件中指明是哪些js文件,并且将isMod设置为true

如果要结合nie.require()(不建议)使用的话

nie.require()语句会被fis3-hook-commonjs插件一起解析,导致我们引用的模块名会被插件认为是无后缀moduleId,进而会帮我们自动匹配模块的路径替换掉之前的模块名导致报错。

例如,src/js目录下有两个文件common.jsapp.js:

// common.js
nie.define('common', function() {
   return {};
});

// app.js
var common = nie.require('common');

编译之后,app.js会变成:

// app.js
var common = nie.require('/src/js/common');

解决办法为把两个文件放在两个不同的目录,例如app.js放在src/js/app/目录下,common.js放在src/js/common/目录下。当然最好的方式是不使用nie.require(),因为我们引入了mod.js,我们可以直接使用AMD的reuiqre()来实现模块化。

demo

demo仓库:gitlab

Reference

  1. fis3-parser-babel
  2. fis3-parser-vue-component
  3. fis3-hook-commmonjs
  4. mod.js
  5. jQuery到Vue的迁移之路


本文来自网易实践者社区,经作者查马纠西授权发布。