fis3 工程化工具应用实践经验

目录
[隐藏]

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 中,我们主要做了三件事情:

  1. 固定基本的配置,包括目录规范、vue/react 编译、es6 编译、less 编译、stylelint、eslint、合并压缩逻辑、线上发布规范等。
  2. 固定了通用依赖插件,保证每一个项目基本依赖插件的一致性,简化了项目内依赖安装配置流程。
  3. 通过自定义插件 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
点赞 (10)
  1. 云养生说道:
    Google Chrome 47.0.2526.108 Google Chrome 47.0.2526.108 Windows 10 x64 Edition Windows 10 x64 Edition

    完全看不懂,只懂简单的ASP

    1. rocalker说道:
      QQbrowser 4.3.4986.400 QQbrowser 4.3.4986.400 Mac OS X  10.13.4 Mac OS X 10.13.4

      同感

  2. node-sass 安装失败的原因说道:
    Google Chrome 58.0.3029.81 Google Chrome 58.0.3029.81 Windows 7 x64 Edition Windows 7 x64 Edition

    这个页面在chrome有时候有点不进去。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code