1. fis3 简介
fis3 是狼厂出产的工程化构建工具,在其厂内得到广泛推广应用,在国内也火了很长一段时间。而在 webpack 和 rollup 大红大紫的当下,fis3 似乎在日新月异的前端圈里渐渐地淡出大众的视野。
暂不说孰优孰劣,好用够用就是最好的。fis3 相对已比较成熟稳定,又太长时间没有新东西拿出来,在 tree shaking 这些技术面前显得有些落后。当前应该仍有不少的团队在持续地使用 fis3,但其使用是如此简单,以至于没有什么太多的东西能拿来不断的吹牛的,来自于第三方的分享是如此之少。另外,fis3 的开源社区生态相对来说比较糟糕,其在国际化的路途上似乎是从未想过要踏出任何一步。不进步就是落后,fis3 显得似乎要没落下去的趋势。扯多了,下面进入本文正题。
2. 使用 fis3
fis3 的使用可简单概括为如下几步:
- 全局安装 fis3
npm i -g fis3
- 在项目目录下安装需要用到的 fis3 插件(可以写入 package.json 中的依赖列表中,也可以全局安装)
- 在项目目录中新建
fis-conf.js
配置文件,书写 fis3 配置 - 执行 fis3 编译命令
下面这张图可以描述 fis3 的基本工作原理:
fis3 通过灵活的插件机制,可以在其工作的各个流程实现不同的功能处理,从而满足多样化的功能需求。fis3 的插件开发也是非常简单的,几行代码都可以实现一个简单却功能完善的插件。
fis-conf.js
配置文件是 fis3 执行的入口。下面是一个可以处理 vuejs、less、stylelint、eslint、es6 的配置示例:
///====================
// 基本配置
///====================
// 设置 NODE_ENV
if ('prod' === fis.project.currentMedia()) {
process.env.NODE_ENV = 'production';
} else {
process.env.NODE_ENV = 'development';
}
fis.set('project.files', [
'view/*.html',
'{resources/static, server}/**',
]);
// npm install [-g] fis3-hook-amd
fis.hook('amd', { // amd配置
globalAsyncAsSync: true, // 都转为同步加载
baseUrl: '/resources/',
paths: {
comm: '/app/comm/', // js common 目录别名
vue: '../node_modules/vue/dist/vue', // vue2 运行+编译 完整版本
},
extList: ['.js', '.es6', '.es', '.vue'],
allowOnymous: true
});
// npm i -g fis3-hook-node_modules fis-hook-commonjs
// 禁用 fis-hook-components,启用 fis-hook-node_modules
fis.unhook('components');
fis.hook('node_modules', {
ignoreDevDependencies: true,
shutup: false
});
// npm i -g fis3-preprocessor-js-require-css fis3-preprocessor-js-require-file
// 添加 css 和 image 加载支持
fis.match('/resources/{app,js,modules}/**.{js,jsx,ts,tsx,es,es6,vue}', {
preprocessor: [
fis.plugin('js-require-css'),
fis.plugin('js-require-file', {
useEmbedWhenSizeLessThan: 1 * 1024 // 小于10k用base64
})
]
});
// 启用 fis-spriter-csssprites 插件
fis.match('::package', {
spriter: fis.plugin('csssprites')
});
//====================
// 文件解析、加载
//====================
// npm install -g fis-parser-less fis3-postprocessor-autoprefixer
// 解析 less
fis.match('*.less', {
parser: fis.plugin('less'),
postprocessor : fis.plugin('autoprefixer', {
"browsers": ['Firefox >= 36', 'Safari >= 8', 'Explorer >= 9', 'Chrome >= 41', "ChromeAndroid >= 4.0"],
"flexboxfixer": true,
"gradientfixer": true
}),
rExt: '.css'
});
// npm i -g fis-parser-node-sass
// 解析 sass
fis.match('*.scss', {
rExt: '.css',
parser: [
fis.plugin('node-sass', {
include_paths: ['static/scss']
})
],
postprocessor: fis.plugin('autoprefixer')
});
// npm install -g fis-parser-babel-latest
// 解析 es6 和 react 文件
const babelConfig = {
rExt: '.js',
parser: fis.plugin('babel-latest', {
sourceMaps: true
})
};
fis.match(/\.es6?$/, babelConfig);
// js 文件也 babel 编译,但排除 .min.js 后缀的文件
fis.match('/resources/{app,js,common,modules}/**.js', babelConfig);
fis.match('**.min.js', {
parser: false
});
// npm install -g fis-parser-vue
// 解析 vue 文件
fis.set('project.fileType.text', 'vue');
fis.match('*.vue', {
rExt: '.js',
parser: fis.plugin('vue')
});
//====================
// lint 校验
//====================
// npm i -g eslint babel-eslint eslint-plugin-react fis-lint-eslint
// 使用 eslint 校验代码。规则参考:http://smocean.github.io/docs/rules/
fis.media('prod').match(/resources\/(app|js)\/.*\.(js|es|es6|es|ts|vue)$/i, {
lint: fis.plugin('eslint', {
ignored: [/\.min\.js$/i],
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"es6": true
},
"envs": ["browser", "es6", "node"],
"globals": {
"__uri": true,
"__inline": true,
"require": true,
"define": true,
"module": true,
"MZ": true,
"console": true
},
"rules": { //0 关闭,1 警告,2 错误
"quotes": [0, "single", "avoid-escape"] //使用单引号,除非为了避免转义
},
"extends": "eslint:recommended",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
},
"plugins": ["html", "vue", "react"],
useEslintrc: false // 不使用 .eslintrc 文件配置
})
});
// npm i -g stylelint fis3-lint-stylelint_d
// sublime text 插件:https://github.com/kungfusheep/SublimeLinter-contrib-stylelint
// stylelint 校验 css/less/scss。规则参考:http://stylelint.io/user-guide/rules/
var stylelintConf = {
fix: false, //为 true 则使用 stylefmt 修正代码(没必要)
syntax: ['scss', 'less', 'sugarss'],
config: {
//"configFile": '../../.stylelintrc',
"ignoreFiles": ['*.min.css'], //忽略部分文件
"extends": ["stylelint-config-standard"], //加载扩展规则。需要 npm install 在项目目录或父级下
"rules": { // 规则配置,会覆盖 extends 中相同的配置项
"comment-empty-line-before": ["always", {
"ignore": ["stylelint-commands", "between-comments"],
}]
}
}
};
fis.media('prod').match('/resources/{app,js}/**.{css,less,scss}', {
lint: fis.plugin('stylelint_d', stylelintConf)
});
fis.media('prod').match('**.min.css', {
lint: false
});
//====================
// 项目目录规范
//====================
//static 静态目录内不作依赖分析
fis.match('/resources/static/**', {
skipDepsAnalysis: true, //hook-amd
ignoreDependencies: true //hook-commonjs
});
//自动加载同名文件
fis.match('/resources/**.{js,es,es6,vue,css}', {
useSameNameRequire: true,
skipBrowserify: true // 默认全部跳过浏览器的 shim
});
// node_modules 目录下的第三方库,为模块化引用
fis.match('/node_modules/**.{js,jsx,ts,tsx,es,es6,vue}', {
isMod: true,
skipBrowserify: false
});
// 项目文件、第三方库
fis.match(/resources\/(app|js|common|lib|modules|node_modules)\/(.*)\.(js|es|es6|vue|jsx)$/i, {
isMod: true,
release: '$&',
moduleId: '$1/$2' // moduleId 简写,去掉 resources 前缀
});
//过滤 .md、.json、.bak 等文件
fis.match(/resources\/(.*)\.(json|md|bak)$/i, {
release: false
});
//不需要任何处理的静态目录(会压缩),无需 fis3 模块化和压缩处理的资源
fis.match('/resources/{pkg,static}/**', {
useHash: false,
isMod: false,
release: '$&'
});
//字体目录只放字体文件,不作 hash 处理
fis.match(/fonts\/(.*)/i, {
useHash: false,
isMod: false
});
//====================
// prod:发外网打包、压缩、加MD5
//====================
// optimize压缩、加MD5
fis.media('prod').match(/resources\/.*\.(js|es|es6|vue)$/i, { //js 压缩
optimizer: fis.plugin('uglify-js', {
compress: {
drop_console: true
}
})
}).match(/resources\/.*\.(css|less)$/i, { // css 压缩
optimizer: fis.plugin('clean-css')
}).match(/resources\/.*\.(jpg|png|gif|bmp)$/i, { // 资源文件添加 hash
useHash: true
}).match('/resources/static/**', { // static 目录不作 md5 处理
useHash: false,
optimizer: false,
isMod: false
}).match('/resources/static/require/**', { // require.js 需要压缩 md5
useHash: true
}).match('/resources/pkg/**', {
useHash: true
}).match('::package', {
packager: fis.plugin('map', {
// 是否输出路径信息,默认为 true
useTrack: false,
// lib 目录第三方插件
'/resources/pkg/js/plugins-lib.js': [
'/resources/lib/**.{js,es,es6,jsx,vue}',
'!resources/lib/tongji/**'
],
// node_modules 第三方插件
'/resources/pkg/js/modules-lib.js': ['/node_modules/**.{js,es,es6,jsx,vue}'],
'/resources/pkg/js/common.js': [
'/resources/{comm,modules}/**.{js,es,es6,jsx,vue}'
]
})
});
//====================
// 线上发布目录规范修正
//====================
fis.match('node-server/(**)', {
skipDepsAnalysis: true, // hook-amd
ignoreDependencies: true, // hook-commonjs
useHash: false,
release: '/node-server/${artifactId}/$1', // 线上目录发布规范兼容
});
fis.match('/resources/(**)', {
release: '/resources/${artifactId}/$1',
});
fis.match('/node_modules/**', {
release: '/resources/${artifactId}/$0'
});
如果你不了解 fis3,可以前往官方大致浏览一遍以快速了解,本文不对其基本原理和入门作进一步的详细介绍。
3. fis3 应用实践
相对来说,fis3 的使用是简单高效的,通过简单的配置或插件扩展,即可以处理绝大部分前端工程化问题。
我们从 2015 年初开始实践基于 nodejs 的前后端分离模式,之后在几乎所有的内部中后台项目中应用,同时开始使用 fis2/fis3 进行工程化构建管理,在 fis 体系的道路上可以说已经走了相当遥远的路程。在长期的应用实践中,我们踩了很多的坑,合理的配置应用是简单又重要的。下面为一些主要的应用经验与技巧列举。
3.1 配置项目按需加载
fis3 可以通过 project.files
来配置默认处理文件,通过 project.ignore
来配置默认忽略的文件。
project.files
列表中的内容会被 fis3 遍历处理。但是它的默认值是 ‘*’,即代表fis-conf.js
目录下的所有文件。
当项目庞大文件繁多时,所有文件都去进行遍历处理肯定会有问题,其处理时长让你等的想死。这个问题有非常多的同学提过 issue 质疑。
我们需要修改 project.files
的取值解决这个问题。具体哪些文件需要 fis3 来处理?
好的方式是以 html
文件为入口。所有在 html 入口文件引用链上的文件都会被加入处理列表中,从而实现了按需加载处理。另外,对于一些非模块化的静态内容,可能不会出现在依赖列表中,但在特定的时刻也会用到,这些也应该加入到项目文件列表中。
所以你可以这样来配置:
fis.set('project.files', [
'view/*.html', //只处理 html,根据 html 引用依赖分析其他文件
'server/**', // node server 内容应发布
'resources/static/**' // 静态资源应发布
]);
project.ignore
列表中的文件 fis3 不会主动去读取处理,除非是被需要处理的文件依赖了。其实配置了按需加载模式后,该项就可以忽略了。
3.2 压缩 moduleId
fis3 可以采用 commonjs 规范编写代码以取得良好的编码体验,最终编译输出为 AMD 规范的内容。
你可以直接书写 es6 module 规范的代码,由 babel 或 typescript 插件编译为 commonjs 规范内容,再交给 fis3 处理为 AMD 规范的最终文件。
AMD 规范需要为每一个依赖文件定义一个引入 Id,例如对于 jquery 的依赖引用:
define('jquery', function() {
var $ = require('jquery');
})
在 fis3 中,moduleId 默认使用文件路径,这使得易于调试定位,但在生产环境下浪费太多了。
但 fis3 提供了自定义 moduleId 功能。我们是这样来做的:在开发环境下,使用默认的路径方式;在生产环境下,使用对文件路径 md5 后取值前 3 或 5 位的方式。示例:
// moduleId 计算,取 path md5 前 3 位,有重复则取前 5 位
const moduleIdList = [];
// let fileCount = 0;
function fnCalcMId(map, path) {
let mId = fis.util.md5(path, 3);
if (moduleIdList.indexOf(mId) > -1) {
mId = fis.util.md5(path, 5);
}
moduleIdList.push(mId);
// console.log(++fileCount, ' - ', path);
return mId;
}
// 项目文件、第三方库
fis.match(/resources\/(app|comm|lib|modules)\/(.*)\.(js|es|es6|vue|jsx|ts|tsx)$/i, {
isMod: true,
moduleId: process.env.NODE_ENV === 'production' ? fnCalcMId : '$1/$2'
});
// node_modules 目录下的第三方库,为模块化引用
fis.match('/node_modules/(**).{js,es,es6,vue,jsx,ts,tsx}', {
isMod: true,
skipBrowserify: false,
moduleId: process.env.NODE_ENV === 'production' ? fnCalcMId : 'npm/$1'
});
3.3 配置文件分类引入
在涉及文件编译、代码校验、目录规范配置、开发和生产环境区分等一系列功能处理的情况下,配置内容相对来说是比较多的。官方示例是都写在 fis-conf.js
中,这会导致该文件多达数百行,维护起来很不方便。我们可以按照功能内容将它分文件书写,然后在 fis-conf.js
中 require 进去。以下为简单的示例说明:
base.js
– 包含 amd 模块化处理、autoprefixer、require js/css、全局基本配置等内容parse.js
– 包含 es6/babel、vue、react、less、sass 等需要预处理语言的文件编译lint.js
– 包含 stylelint 和 eslint 配置folder-standard.js
– 项目目录规范配置相关dev.js
– 开发模式下的配置prod.js
– 生产环境下的配置qa.js
– 与后端一起测试联调环境下的配置
3.4 自定义插件
除了官方出产的插件外,更多的需求需要用到第三方插件。但是对于插件的质量却没有渠道有效地把控,出了问题无法及时去获得改进解决。我们的建议是,对于关键性功能需求,参考第三方类似插件,根据需要开发一个适合自己项目规范需求的插件。好在 fis 的插件体系非常简单,开发一个插件的成本是非常低。这样即可以保证核心功能依赖由我们自己把控。
例如我们自己维护了 es6 编译插件 fis-parser-babel-latest,stylelint 校验插件 fis3-lint-stylelint_d。另外自定义了插件 mz-node-cli,以让符合我们项目规范特点的项目工程化流程简单易用。
3.5 fis3 解决方案封装
封装解决方案是为了统一团队开发规范。根据 fis3 提供的 API,可以很方便的进一步封装一个适合团队的工具。具体可参考:fis3 高级使用。
我们根据其规范,封装了 mz-node-cli,全局化了一个 mznode
命令。在 mznode
中,我们主要做了三件事情:
- 固定基本的配置,包括目录规范、vue/react 编译、es6 编译、less 编译、stylelint、eslint、合并压缩逻辑、线上发布规范等。
- 固定了通用依赖插件,保证每一个项目基本依赖插件的一致性,简化了项目内依赖安装配置流程。
- 通过自定义插件 mznode-command-start 实现项目构建编译的便捷性。
这样封装的好处是,制定好了团队一致性的目录规范、技术栈规范、插件依赖的一致性,统一处理解决插件依赖等问题,大大的简化了具体项目内的配置。于是团队成员可以快速的开始一个新的项目,不必在工程化配置上花费太多的精力。
3.6 其他问题
存在 bug 是在所难免的,在将近两年的实践过程中,遇到的小问题比较多。这些问题可能在新的版本更新中就不存在了,也有可能会偶尔的又出现。但一般来说,都可以有对应的降级处理方式。
- 开启了
-wl
参数,页面不停的刷新:排查是否有循环编译产生的配置,予以修改;暂时停止使用 -l 参数 - 不在初始依赖链上的文件,新引入时容易出现 404 找不到:强制刷新页面;关闭 fis3,重新执行构建
- more…
以上即为我们使用 fis3 过程的一些主要的使用经验,希望 fis3 能够有更好的发展。如你有更多更好的使用心得,欢迎留下评论一起探讨。
4 更多阅读
- http://fis.baidu.com
- http://fex.baidu.com/blog/2016/04/develop-react-with-fis3/
- https://zhuanlan.zhihu.com/p/20933749
完全看不懂,只懂简单的ASP
同感
这个页面在chrome有时候有点不进去。