前端工程化之自动化构建

    技术2025-07-06  29

    自动化构建

    源代码自动转换为生产环境代码

    NPM Scripts脚本实现自动化

    利用npm scripts脚本实现自动化,npm scripts原理参照阮一峰老师博客

    钩子机制实现构建流: pre-<name>:该命令在name命令之前先启动post-<name>:该命令在name命令之后才启动 npm-run-all模块:同时并行执行多个脚本命令,如npm-p build serve同时启动build和serve脚本命令

    常用自动化构建工具

    gulp:基于虚拟文件对象的流程,内存中操作文件,而不需要和磁盘交互。grunt:基于临时文件的流程,每一步都要将文件内容写入磁盘,构建速度慢fis:百度推出的前端构建系统,捆绑套餐(集成了很多常用构建流程)

    Grunt

    安装npm install grunt创建gruntfile.js,用来导出grunt要运行的任务在命令行中使用npx grunt <taskname>来启动任务,默认任务则直接使用npx grunt启动即可。 //gruntfile.js module.exports = grunt => { // registerTask函数的第二个参数是可选的 grunt.registerTask('foo', '任务描述', () => { }) grunt.registerTask('bar', () => { }) // default任务由串行的foo和bar任务组成 grunt.registerTask('default', ['foo', 'bar']); // grunt默认是同步操作,异步操作需要先用this.async()生成一个done函数引用 // 在异步任务结束后调用done,来告诉grunt异步任务完成了。 grunt.registerTask('async-task', function() { const done = this.async(); setTimeout(() => { // 具体任务代码 done(); }, 1000); }) // 标记失败的任务,如果任务失败,则后续任务不再执行 // 通过在命令行使用--force来强制执行后续任务 grunt.registerTask('fail', () => { // 在函数体中return false来标记任务失败 return false; }) // 异步任务的失败不能直接返回false,而是在done中传入false grunt.registerTask('async-fail', function() { const done = this.async(); setTimeout(() => { // 标记异步任务失败,给done函数传入false来调用 done(false); }, 1000); }) }

    Grunt的配置选项方法

    module.exports = grunt => { // 添加配置项 grunt.initConfig({ // 配置选项的键一般与任务名一致,值为任意类型数据 foo: { bar: '123' } }) grunt.registerTask('foo', () => { grunt.config('foo.bar'); }) }

    Grunt多目标模式任务

    多目标模式,可以让任务根据配置形成多个子任务在命令行中使用npx grunt build启动多目标任务时,会显示出两个子任务的执行也可以直接执行子任务npx grunt build:css module.exports = grunt => { // 为多目标模式的任务指定配置,从而形成不同目标的子任务 // 键与任务名一致,值为对象,且属性名为子任务名 grunt.initConfig({ build: { // options不作为子任务出现,而是作为build任务的配置选项 options: { foo: 'bar' } // 除了options之外,其他的键值对都会作为子任务出现 css: 'css', js: 'js', other: { // 子任务的options会遮蔽父任务的options options: { foo: 'other' } } } }) grunt.registerMultiTask('build', function() { // this.options()方法获取到build任务的options选项对象 console.log(this.options()); // this.target获取子任务名,this.data获取子任务名对应的值; console.log(`build task target: ${this.target}, data: ${this.data}`); }) }

    Grunt插件的使用

    安装插件,绝大多数插件的命名规范都是grunt-contrib-<name>在gruntfile.js中导入插件提供的任务根据插件文档进行配置使用npx grunt <name>来运行插件任务 // 安装 npm install grunt-contrib-clean // gruntfile.js module.exports = grunt => { // 为插件进行配置,这里配置的是多目标任务 grunt.initConfig({ clean: { temp: 'temp/app.js' } }) // 导入插件提供的任务 grunt.loadNpmTasks('grunt-contrib-clean'); }

    常用的Grunt构建插件演示

    安装load-grunt-tasks,用于减少loadNpmTasks的调用安装grunt-sass和sassnpm install grunt-sass sass -D,编译sass文件安装grunt-babel和@babel/core @babel/preset-env,编译ES6+安装grunt-contrib-watch,启动任务监控 const sass = require('sass'); const loadGruntTasks = require('load-grunt-tasks'); module.exports = grunt => { grunt.initConfig({ sass: { // 为sass任务提供选项对象,这里指定了sass的编译环境和sourceMap文件 options: { sourceMap: true, implementation: sass } // 为sass任务提供一个目标任务 main: { files: { 'dist/css/main.css': 'src/scss/main.scss' } } }, babel: { options: { sourceMap: true, presets: ['@babel/preset-env'] } main: { files: { 'dist/js/app.js': 'src/js/app.js' } } }, // watch只负责监控,第一次启动watch任务不会运行其他tasks watch: { // js子任务,监控js文件,有更改则运行babel任务 js: { files: ['src/js/*.js'], tasks: ['babel'] }, // css子任务,监控scss文件,有更改则执行sass任务 css: { files: ['src/scss/*.scss'], tasks: ['sass'] } } }) // grunt.loadNpmTasks('grunt-sass'); // 自动加载所有grunt插件中的任务 loadGruntTasks(grunt); // 用默认任务来第一次任务 grunt.registerTask('default', ['sass', 'babel', 'watch']) }

    Gulp

    基本使用

    安装gulpnpm install gulp -D,同时也会自动安装好gulp-cli创建gulpfile.js作为gulp的入口文件,由外界启动的tasks挂载到exports对象上(遵循CommonJS规范),或者作为私有任务不暴露给外部。命令行运行npx gulp <task-name>启动任务。
    任务定义与挂载
    每一个任务都是一个函数gulp默认都是异步任务,因此,任务函数接收一个done参数,用于在任务结束时调用done()来通知gulp任务结束。任务函数如果返回流、Promise等,可以不调用done。任务函数通过挂载到exports上才能通过命令行启动运行,没有挂载到exports上的都是私有函数,只能由gulp在内部处理。
    任务的组合
    series顺序执行
    const { series } = require('gulp'); exports.default = series(task1, task2, task3); // 顺序执行这三个任务
    parallel并行执行
    const { parallel } = require('gulp'); exports.default = parallel(task1, task2, task3); // 并行执行这三个任务
    Gulp异步任务的三种方式

    如何通知gulp异步任务的完成

    回调方式
    exports.callback = done => { console.log('回调函数方式'); done(); // 表示异步任务完成 } // 回调函数与node.js中的一致,都是错误优先方式; exports.callback_err = done => { console.log('出现错误时的异步任务'); // 如果任务出错,则给done传入的第一个参数为异常对象 done(new Error('task failed')) }
    Promise方式
    exports.promise = () => { console.log('promise task'); return Promise.resolve(); } exports.promise_err = () => { console.log('promise task failed'); return Promise.reject(new Error('task failed')); } // 使用async函数的方式,只要node环境支持就可以使用 const timeout = (time) => { return new Promise(resolve => { setTimeout(resolve, time); }) } exports.async_task = async () => { await timeout(1000); console.log('async task'); }
    返回流的方式

    在任务中返回流是gulp中最常用的异步任务结束方式。在gulp中最频繁的操作就是读取文件流、转换文件流、写入文件流等待,返回一个文件流,实际上会在流的end事件触发时,通知gulp异步任务完成了。因此,返回流、返回EmitEvent类型的对象都是可以的。

    const fs = require('fs'); exports.stream = () => { const readStream = fs.createReadStream('package.json'); const writeStream = fs.createWriteStream('temp.txt'); readStream.pipe(writeStream); return readStream; } // 等同于如下的方式 exports.stream = (done) => { const readStream = fs.createReadStream('package.json'); const writeStream = fs.createWriteStream('temp.txt'); readStream.pipe(writeStream); readStream.on('end', () => { done(); }); }

    Gulp构建过程的核心工作原理

    // gulpfile.js // 使用node.js的原始模块来模拟gulp的工作 const fs = require('fs'); // 导入转换流 const { Transform } = require('stream'); export.default = () => { // 创建文件读取流 const read = fs.createReadStream('normalize.css') // 创建文件写入流 const write = fs.createWriteStream('normalize.min.css'); // 创建转换流,用来去除注释,删除空白字符 const transform = new Transform({ transform: (chunk, encoding, callback) => { // 核心转换过程 // chunk即读取流中的内容(Buffer) const input = chunk.toString(); // 将字节数组转为字符串 const output = input.replace(/\s+/g, '') // 删除空白字符 .replace(/\/\*.+?\*\//g, '') // 删除注释 callback(null, output); // 回调函数 } }) // 把读取流传入转换流,再传入写入流 read .pipe(transform) .pipe(write); // 返回读取流 return read; }

    Gulp文件操作API和插件的使用

    构建流程:Gulp读取流 + 插件的转换流 + Gulp的写入流 Gulp采用同名文件覆盖的策略。

    # 安装gulp-clean-css插件,压缩CSS代码 npm install gulp-clean-css --dev # 安装gulp-rename插件 npm install gulp-rename --dev const { src, dest } = require('gulp'); const cleanCss = require('gulp-clean-css'); const rename = require('gulp-rename'); exports.default = () => { return src('src/normalize.css') .pipe(cleanCss()) .pipe(rename({ extname: '.min.css' })) .pipe(dest('dist')); }

    Gulp案例

    样式编译
    # 安装gulp-sass用来编译scss文件,同时会安装node-sass # node-sass是一个C++模块,有时候国外的源下载很慢,需要为这个包配置单独的镜像源 npm install gulp-sass --save-dev src(globs, [options]) options.base:默认的base为glob base,即特殊字符之前的路径。在dest()写入流时,会将base删去。因此源文件的目录结构在编译后就会丢失。如果手动指定base,则可以保留源文件的目录结构。 // gulpfile.js const { src, dest } = require('gulp'); const sass = require('gulp-sass'); // 建立style私有任务 const style = () => { // { base: 'src' }指定一个基准路径,这样在生成文件到dist路径下时 // src后面的路径会原封不动复制到dist路径下,这里涉及到了glob base概念 // 具体可以查阅gulp的官方文档 return src('src/assets/styles/*.scss', { base: 'src' }) /* sass()默认不会转换_开头的.scss文件 因为_开头的sass文件是作为其他.scss的依赖文件存在的 会直接被其他.scss文件引入,然后再一起编译 outputStyle配置属性来指定输出文件的格式 */ .pipe(sass({ outputStyle: 'expanded' })) .pipe(dest('dist')) } module.exports = { style }
    脚本编译
    # 安装gulp-babel编译ES6+,这里还需要安装@babel/core、@babel/preset-env npm install gulp-babel @babel/core @babel/preset-env --save-dev // gulplfile.js const babel = require('gulp-babel'); const script = () => { return src('src/assets/scripts/*.js', { base: 'src' }) .pipe(babel({ presets: [ '@babel/preset-env' ] })) .pipe(dest('dist')) } module.exports = { style, script }
    模板文件(HTML文件)的编译

    这里使用了swig模板引擎

    # 安装gulp-swig npm install gulp-swig --save-dev // gulpfile.js const { series, parallel } = require('gulp'); const swig = require('gulp-swig'); // data作为模板引擎的渲染数据上下文对象,在后面的封装工作流中data会被移除 const data = { //这里是具体的数据 }; const page = () => { // 这里的base设置其实没有必要,但为了统一 return src('src/*.html', { base: 'src' }) .pipe(swig({ data: data, // cache:false保证swig引擎的缓存机制不生效,这样页面的修改就可以在开发服务器上实时反映出来 cache: false })) .pipe(dest('dist')) } // module.exports = { style, script, page } /* 创建一个并行的组合任务 */ const compile = parallel(style, script, page) module.exports = { compile }
    图片和字体文件的转换

    图片进行压缩,对图片中的元信息进行删除

    # 安装gulp-imagemin压缩图片,无损压缩,svg图片做了代码压缩格式化 npm install gulp-imagemin --save-dev // gulpfile.js const { series, parallel } = require('gulp'); const imageMin = require('gulp-imagemin'); const image = () => { return src('src/assets/images/**', { base: 'src' }) .pipe(imageMin()) .pipe(dest('dist')) } const font = () => { return src('src/assets/fonts/**', { base: 'src' }) .pipe(imageMin()) .pipe(dest('dist')) } const compile = parallel(style, script, page, image, font) module.exports = { compile }
    其他文件以及文件清除
    # 安装del,用来清除文件 npm install del --save-dev // gulpfile.js const { series, parallel } = require('gulp'); const del = require('del'); // 额外任务,就是复制public目录下的所有文件到dist目录下 const extra = () => { return src('public/**', { base: 'public' }) .pipe(dest('dist')) } const clean = () => { return del(['dist']) } // 先清除,然后再构建 const build = series(clean, parallel(compile, extra)); module.exports = { compile, build }
    自动载入插件
    # 安装gulp-load-plugins,提供自动加载插件的功能,这样避免gulpfile.js中导入插件的语句太多 npm install gulp-load-plugins --save-dev // gulpfile.js const loadPlugins = require('gulp-load-plugins'); // 使用loadPlugins方法将所有插件导入,返回一个plugins命名空间 // 所有插件都成为plugins的属性 // 属性名为去掉gulp-前缀,如果插件名为gulp-xxx-yy,则会将xxx-yy转为驼峰命名方式xxxYy // 代码里的插件都需要使用plugins.xxx来引用 const plugins = loadPlugins();
    开发服务器与热更新
    # 安装browser-sync npm install browser-sync --save-dev // gulpfile.js const { series, parallel, watch } = require('gulp'); const browserSync = require('browser-sync'); // 创建一个开发服务器 const bs = browserSync.create(); const serve = () => { // gulp.watch来监控以下路径下的文件,如果文件更新则启动对应的任务 watch('src/assets/styles/*.scss', style); watch('src/assets/scripts/*.js', script); watch('src/*.html', page); /* 图片、字体和public目录下的文件反复去编译是没有意义的,只需要在上线前编译一次即可 因此,没必要在开发阶段每次都编译到dist目录下 在开发阶段,只要保留在原来的src和public目录下即可,让服务器去serve这些资源本来的目录*/ /* watch('src/assets/images/**', image); watch('src/assets/fonts/**', font); watch('public/**', extra); */ // 当这些图片、字体等静态资源有更新时,让浏览器自动刷新获取这些更新的资源 watch([ 'src/assets/images/**', 'src/assets/fonts/**', 'public/**' ], bs.reload); // 配置开发浏览器 bs.init({ /* 关闭服务器与浏览器已连接的提示 */ notify: false, // 配置服务器端口 port: 8080, // 取消自动打开浏览器 open: false, // 标识browser-sync监控的文件,如果有更改则自动热更新浏览器 files: 'dist/**', // 配置服务器的信息 server: { /* 配置网站根目录,这里是dist目录,src和public目录用来在开发阶段 serve那些只在上线前编译一次的静态资源,如图片字体等 */ baseDir: ['dist', 'src', 'public'], // 配置网站中的路由,key是要匹配的url,value是以当前工作目录的相对路径 // 这里将url: /node_modules的路径映射到node_modules // 从而可以访问到项目node_modules路径下的资源 // 这里之所以这样配置,是因为我们引用了项目node_modules中的资源,但没有把该资源复制到dist目录下 routes: { '/node_modules': 'node_modules' } } }) } // 编译html、CSS和JS文件 const compile = parallel(style, script, page) // 上线之前的构建任务build const build = series(clean, parallel(compile, image, font, extra)); // 开发阶段的编译任务,主要就是编译完HTML、CSS和JS后,开启服务器 const develop = series(compile, serve); module.exports = { compile, build, serve }
    useref文件引用处理

    在这里,我们将上文中在HTML文件中引用的node_modules中的bootstrap.css文件处理一下。

    useref的使用方式

    将HTML文件中的CSS和JS文件引用进行合并(但不压缩),文件流传递下去解析HTML文件中的build block语法规则来进行文件合并,解析之后在新生成的HTML文件中将build block规则注释去掉生成引用了合并文件的新的HTML文件,以及合并后的文件。build blocks规则如下所示: <!-- build:<type>(alternate search path) <path> <parameters> --> ... HTML Markup, list of script / link tags. <!-- endbuild --> <type>:css | js | remove,remove只会移除注释,并不产生新文件alternate search path:默认情况下,输入文件的路径是相对于当前解析的文件。alternate search path提供一个更改该路径的值。path:表示输出的文件路径,也就是合成后的文件路径parameters:表示额外的参数

    例如: 处理前的HTML:

    css/one.css和css/two.css合并为css/combined.cssscripts/one.js和scripts/two.js合并为scripts/combined.js <html> <head> <!-- build:css css/combined.css --> <link href="css/one.css" rel="stylesheet"> <link href="css/two.css" rel="stylesheet"> <!-- endbuild --> </head> <body> <!-- build:js scripts/combined.js --> <script type="text/javascript" src="scripts/one.js"></script> <script type="text/javascript" src="scripts/two.js"></script> <!-- endbuild --> </body> </html>

    处理后的HTML,引用了合并后的文件

    <html> <head> <link rel="stylesheet" href="css/combined.css"/> </head> <body> <script src="scripts/combined.js"></script> </body> </html> # 安装gulp-useref npm install gulp-useref --save-dev // gulpfile.js const useref = () => { return src('dist/*.html', { base: 'dist' }) .pipe(plugins.useref({ /* searchPath用来标识那些在HTML文件中引用的资源路径 或者说在项目中去哪里找这些引用的资源 相对于当前工作目录 */ searchPath: ['dist', '.'] })) .pipe(dest('dist')) } module.exports = { clean, compile, build, develop, useref }
    文件压缩(HTML、CSS和JS)
    延续自上文的useref插件,从useref插件返回的流中开始压缩工作。由于三种文件采用不同的压缩插件,因此需要使用gulp-if来进行文件类型判断。 # 安装压缩包 npm install gulp-htmlmin gulp-uglify gulp-clean-css --save-dev # 安装gulp-if来判断 npm install gulp-if --save-dev // gulpfile.js /* 注意,在运行useref任务之前,可能需要先运行编译compile任务 因为useref任务取决于HTML文件中的build blocks语法注释 如果之前已经运行过useref,那么HTML文件中就没有了build blocks注释 */ const useref = () => { return src('dist/*.html', { base: 'dist' }) .pipe(plugins.useref({ /* searchPath用来标识那些在HTML文件中引用的资源路径 或者说在项目中去哪里找这些引用的资源 相对于当前工作目录 */ searchPath: ['dist', '.'] })) .pipe(plugins.if(/\.js$/, plugins.uglify())) .pipe(plugins.if(/\.css$/, plugins.cleanCss())) .pipe(plugins.if(/\.html$/, plugins.htmlmin({ collapseWhitespace: true, // 将HTML文件中<style>和<script>中的代码也压缩 minifyCSS: true, minifyJS: true }))) // 这里把dist换成了release,防止出现在同一个文件夹下读取和写入造成的冲突 .pipe(dest('release')) } module.exports = { clean, compile, build, develop, useref }
    重新规划构建过程

    前面由于useref的使用,为了防止写入与读取的冲突,导致更换到release目录作为发布目录,但release目录中没有图片字体等静态资源,这些静态资源是构建在dist目录下的,因此需要对构建过程重新规划一下。 整个构建过程:编译 -> 临时目录temp: useref + 图片等静态资源编译 -> dist目录

    // gulpfile.js /* clean需要改为del(['dist', 'temp']) style、script、page这几个任务需要修改为dest('temp') serve任务的baseDir也要改为'temp'*/ // 将build任务修改 const build = series( clean, parallel( series(compile, useref), image, font, extra ) )
    任务导出与私有任务
    在完成整个构建流程后,对哪些任务需要导出给外部使用(接口),哪些任务作为内部私有任务进行规划。我们这里选择了clean、develop、build三个任务导出给外部作为接口使用,其他私有任务都封装在这三个任务中。在package.json中使用npm scripts来运行gulp任务。

    封装工作流:在多个项目中复用自动化构建过程

    封装工作流:将完成的一套构建流程在多个项目中复用(不是简单复制粘贴) 将已经构建好的gulpfile.js、gulp和依赖项作为一个新的NPM模块(比如我们命名为gulp-build)发布,在新的项目中安装gulp-build模块即可使用封装好的工作流。

    封装流程
    将gulpfile.js的内容添加到gulp-build模块的入口文件index.js中,通常目录结构为lib/index.js。开发依赖项拷贝到gulp-build模块的依赖项中,这样当我们安装gulp-build模块时,就会自动安装那些依赖模块。入口文件中还应该提供一个配置项,用于针对不同项目的不同情况(我们在进行模板编译的时候需要这个配置项来作为模板数据的上下文对象)。每个项目都可以提供一个配置文件,gulp-build模块读取该配置文件,与自身的默认配置项做一个合并,作为整体的配置项。 抽象路径的配置:在原来的gulpfile.js中,对于文件的读取和写入的路径都是固定的,无法针对不同项目的目录结构灵活配置。因此,我们将这些路径做一个抽象配置,在默认配置对象中定义默认的路径,通过读取项目中的配置文件来替换这些路径。 包装gulp-cli,构建模块自己的CLI。 因为我们的gulp-build模块内部已经有gulpfile.js文件(就是模块的入口文件index.js)了,没有必要在使用环境中再次提供一个gulpfile.js来导入gulp-build中的index.js文件,因此,我们只需要在使用环境下,让gulp运行gulp-build中的index.js即可,这就需要提供一个命令行接口在使用环境下直接运行index.js。gulp-cli提供了在命令行中传入参数的方式来指定gulpfile.js的路径gulp <task-name> --gulpfile <gulpfile-path>。同时,可能还需要对工作目录做一下指定(因为gulp默认将gulpfile.js所在路径作为工作目录)gulp <task-name> --gulpfile <gulpfile-path> --cwd <cwd-path>。但是在命令行中传参数会显得很麻烦,所有我们给gulp-build模块提供一个自定义CLI用以代替,将命令行传参转换为CLI脚本文件的执行。CLI的入口脚本文件gulp-build.js一般放在包的bin目录。 package.json中bin字段用以指定CLI的方式执行的入口文件,main字段表示用require()加载模块时的入口文件。 // bin/gulp-build.js,CLI脚本文件 #!/usr/bin/env node // process.argv是一个数组,其中数组元素包含了从命令行传递来的参数 process.argv.push('--cwd'); // 命令行参数名 process.argv.push(process.cwd()); // 参数值 process.argv.push('--gulpfile'); // 参数名 // 参数值,这里使用require.resolve()返回模块入口文件的绝对路径。 // ..是相对路径,向上找到模块根路径,然后会自动查看package.json中的main字段 process.argv.push(require.resolve('..')); // 启动gulp,实际上都是require('gulp-cli')() // gulp-cli的入口文件导出了一个run的方法,这里都是执行run方法 require('gulp/bin/gulp') // gulp-build模块的入口文件index.js, // 这里只写出对上文gulpfle.js的补充代码 const cwd = process.cwd(); // 获取执行命令的当前工作目录 let config = { // 这里是默认配置的代码 // 对路径的默认配置选项 build: { } }; // 声明一个配置项 try { const loadConfig = require(path.join(cwd, 'page.config.js')); config = Object.assign({}, config, loadConfig); } catch(e) { // } /* 如果在测试环境中运行构建时出现错误信息,说找不到某个模块, 是因为配置时,只声明了该模块的名称, 则会去测试环境的node_modules目录下找,肯定找不到。 因为该模块并不是在测试环境的node_modules目录下, 而是在gulp-build模块的node_modules目录下。 因此,需要在配置时, 将声明模块的语句改为require(<package-name>)的形式, 这样在工作时, require()语句会去gulp-build模块的node_modules目录下找<package-name>模块, 因为我们的index.js是在gulp-build模块里。*/ // 这里以@babel/preset-env演示一下 // 原来的代码,声明模块名称,这样babel就会在当前工作目录的node_modules下找模块 .pipe(plugins.babel({ presets: ['@babel/preset-env'] })); /* 现在更改后的代码,用require()来加载模块, 这样就直接加载了该模块给babel用,不需要babel再自己加载了 */ .pipe(plugins.babel({ presets: [ require('@babel/preset-env') ] }));
    测试该构建模块
    测试gulp-build模块,使用npm link到全局,然后在测试环境的项目下npm link gulp-build。没有包装gulp-cli时 测试项目下,在gulpfile.js中导入gulp-build包,因为gulp-build包的入口文件就是原来的gulpfile.js,所以gulp-build包默认导出了一些任务(这里导出了clean、develop、build三个任务)。安装gulp-cli,从而可以在命令行启动gulp安装gulp,从而可以启动任务 包装gulp-cli,有了自己的CLI时,就可以直接使用了 // 测试项目中的package.json "scripts": { "clean": "gulp clean", "develop": "gulp develop", "build": "gulp build" } // 测试项目中的gulpfile.js module.exports = require('gulp-build'); // 如果想要在终端看到任务名字与对应的执行情况,需要单独导出任务名称, // 这样gulp才能从gulpfile.js中得到这些任务名称。 // 而不是像上面的直接导出一个require的结果。

    FIS

    高度集成常见的构建流程内置webServer

    基本使用

    # 安装 npm install fis3 -g --save-dev 资源定位:将开发文件中的相对路径,在构建后,生产文件中变为绝对路径。使用配置文件进行声明式构建流程 // fis的配置文件 fis.match('*.{js, scss, png}', { release: 'assets/$0' }) fis.match('**/*.scss', { rExt: '.css', parser: fis.plugin('node-sass'), optimizer: fis.plugin('clean-css') }) fis.match('**/*.js', { parser: fis.plugin('babel-6.x'), optimizer: fis.plugin('uglify-js') })
    Processed: 0.014, SQL: 9