自从上次迁移到vue之后(详见jQuery到Vue的迁移之路),还有个开发体验上的问题没有解决,就是没法用vue官方推荐的单文件组件的方式来开发组件。原因是官方推荐的构建工具为webpack,提供了专用的vue-loader
去解析x.vue
文件。而我们目前没法用的主要原因是(我自己觉得)在线上机器装一些其他npm包不太现实,会有其他影响,所以就没有去找相关的方案。知道前几天主管告诉我线上机器装插件没什么问题,想装直接联系相关同事就可以。于是,手又开始痒了,单文件组件的日子要来了。
这个插件是fis3在编译单文件的parse阶段的babel插件,作用就是babel的作用咯,编译es6代码。
这个插件的作用跟webpack中的vue-loader
的作用类似,就是将x.vue
文件进行编译,产出js文件或者js+css文件。
由于使用fis3-parser-babel
插件之后,我们的es6的module语法会被编译成浏览器依旧无法解析的commonjs
语法,因此需要这个插件来将commonjs
所在的模块包装成AMD模块,再由下面会提到的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.css
和components.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也可以。
根据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()
语句会被fis3-hook-commonjs
插件一起解析,导致我们引用的模块名会被插件认为是无后缀moduleId,进而会帮我们自动匹配模块的路径替换掉之前的模块名导致报错。
例如,src/js
目录下有两个文件common.js
和app.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仓库:gitlab
本文来自网易实践者社区,经作者查马纠西授权发布。