共计 9560 个字符,预计需要花费 24 分钟才能阅读完成。
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
相关文章:











