sk2-gulp-cli

1.0.6 • Public • Published

sk2-gulp-cli

项目主页

介绍

一个内置gulp-cli & gulpfile & 灵活配置的工作流cli

项目结构

└── sk2-gulp-cli ······项目根目录
   ├─ bin ·············node命令目录
   │  ├─ index.js ·····bin入口文件
   ├─ lib ·············gulp相关文件
   │  ├─ cmdPromise.js ····将node-cmd.run函数包装成Promise函数exec
   │  ├─ config.js ····gulefile相关配置,主要为路径默认配置
   │  ├─ data.js ······页面相关默认配置
   │  ├─ gulpfile.js ··主gulpfile文件
   │  ├─ index.js ·····导出gulefile,main入口文件
   ├─ .eslintrc.js ····eslint默认配置,可以被项目的eslint配置覆盖
   ├─ .gitignore ······git忽略文件配置
   ├─ .npmrc ··········npm镜像下载地址
   ├─ LICENSE ········证书
   ├─ lint.yml ········暂不使用
   ├─ package.json ····npm包说明文件
   ├─ project.config.js ····项目默认配置样本,为lib中config和data的合集。
                            本项目中无用,需在使用项目中添加
   ├─ README.md ·······项目说明
   

使用说明

###安装

  1. npm install sk2-gulp-cli -Dyarn add sk2-gulp-cli -D
  2. sk2-gulp-cli taskName, 可用命令如下

###命令

sk2-gulp-cli lint

scss/js文件lint检查

sk2-gulp-cli compile

scss/js/html文件编译。编译后的css/js/html会被放入temp文件夹中。

sk2-gulp-cli serve

使用内置服务器预览、监听、调试代码。编译后的css/js/html会被放入temp文件夹中。

参数

  • open: 是否在启动服务器时打开浏览器窗口, 默认: false
  • port: 设置端口号, 默认: 2080

sk2-gulp-cli build

打包项目并将文件放入dist目录. 推荐使用production模式对代码/资源进行压缩以达到最佳效果。

options

  • production: production模式, 默认: false
  • prod: 等同于production

sk2-gulp-cli start

使用内置服务器预览production模式打包的项目以获取真实的线上浏览体验。 这个命令其实是sk2-gulp-cli serve --prod的别名。 注意:在这个模式下,源文件监听/调试将被关闭。如果你想要调试,请使用sk2-gulp-cli serve

参数

  • open: 是否在启动服务器时打开浏览器窗口, 默认: false
  • port: 设置端口号, 默认: 2080

sk2-gulp-cli deploy

将打包的dist文件夹push到git仓库

参数

  • branch: 要push的分支名, 默认: 'gh-pages'

sk2-gulp-cli clean

清空dist & temp文件

配置

你可以通过在自己的项目根目录新建一个project.config.js,根据自己的需要来覆盖这些配置。 默认配置如下。项目根目录提供了一份project.config.js,可以直接复制它到你的项目根目录进行修改即可。

module.exports = {
  data: {
    menus: [
      {
        name: 'Home',
        icon: 'aperture',
        link: 'index.html'
      },
      {
        name: 'Features',
        link: 'features.html'
      },
      {
        name: 'About',
        link: 'about.html'
      },
      {
        name: 'Contact',
        link: '#',
        children: [
          {
            name: 'Twitter',
            link: 'https://twitter.com/w_zce'
          },
          {
            name: 'About',
            link: 'https://weibo.com/zceme'
          },
          {
            name: 'divider'
          },
          {
            name: 'About',
            link: 'https://github.com/zce'
          }
        ]
      }
    ],
    pkg: require('./package.json'),
    date: new Date(),
  },
  config: {
    SRC: 'src',
    DIST: 'dist',
    TEMP: '.tmp',
    PUBLIC: 'public',
    PATHS: {
      style: 'assets/styles/*.scss',
      script: 'assets/scripts/*.js',
      page: '*.html',
      image: 'assets/images/**',
      font: 'assets/fonts/**',
    }
  }
}

实现

gulpfile相关任务的实现

lint的实现

lint任务分为styleLint和scriptLint两个子任务,任务的实现相对简单, 分别定义两个任务,引入对应的lint插件,调用相应api即可。以下是scriptLint的实现

const scriptLint = () => {
    return src(PATHS.script, srcConfig)
        //执行lint
        .pipe(plugins.eslint())
        //输出lint结果至控制台
        .pipe(plugins.eslint.format())
        // 如果报错终止执行
        .pipe(plugins.eslint.failAfterError())
}

在使用eslint插件时需注意的是,当手动传入参数至gulp-eslint插件时,插件的参数格式与.eslintrc.js不匹配。 例如,gulp-eslint插件的envs,globals属性均为数组形式,而.eslintrc.js对应的属性为对象。 因此更好的方式应该是不处理参数,让插件在运行时自动读取运行时项目根目录的.eslintrc.js

const lint = parallel(styleLint, scriptLint);

在实现lint命令时使用parallel并行执行,一是缩短时间,二是因为任何一项lint任务一旦报错都无需往下继续。 当然这样做也有一个缺点,子任务的报错日志会交替输出,阅读上有点麻烦。

compile的实现

compile任务分为css/js/html三个子任务,输出半成品的css/js/html文件,配合useref/watcher任务使用。 以js任务为例,实现如下

// script编译
const script = () => {
    return src(PATHS.script, srcConfig)
        .pipe(plugins.babel({
            // 使用require引用,当变成cli时,require找到的是当前cli下的node_modules里的包
            presets: [require('@babel/preset-env')]
        }))
        .pipe(dest(TEMP))
        // 与watcher中的watch任务配合,实现浏览器刷新
        .pipe(server.reload({stream: true}))
}

在实现compile子任务时需要注意以下几点

  • 子任务生成的文件均输出到临时文件夹temp而不是dist目录。这样做有以下几点原因
  1. 因为compile生成的都是半成品文件,它们并不具备在生产环境运行的条件,比如生成的js和css文件没有经过压缩,而且没有生成依赖库文件(vendor.css/js), html文件在没有经过useref处理时也没有真正生成对应的依赖库引用。因此它们不能被放入意味着生产的dist包里
  2. 这些半成品可以配合useref任务生成真正的生产环境代码。而在使用useref时, 从temp目录读取半成品文件,然后经过处理写入dist,可以避免读写冲突
  • 这些半成品文件可以配合watcher任务使用,提高在调试时的编译效率。因此需要在子任务的最后添加server.reload
  • compile任务采用并行执行,以提高效率
// 编译,并行执行
const compile = parallel(style, script, page);

build的实现

build任务的实现如下

// 打包命令,需要先clean,然后并行执行(css/js/html的编译与引用查找,以及其他静态资源的压缩)
const build = series(clean, parallel(series(compile, useref), image, font, extra));

由于build需要区分开发/生产模式,因此在useref/image/font等子任务中均需要做相应处理(不包括compile)。 以下代码通过插件从命令行中获取isProd。当isProd为true时, 需要对所有的资源进行压缩/混淆/查找引用等处理

// 转化命令行参数为一个对象
const args = require('node-args-parser')(process.argv);
console.log('args', args);
// 处理相关命令行参数
// 提取prod参数供后续使用
const isProd = args['-production'] || args['-prod'] || false;

其中useref的实现如下

const useref = () => {
    return src(PATHS.page, tempConfig)
    // tempConfig中的cwd:TEMP会将useref的工作目录改为TEMP,
    // 加上..才是项目根目录,从而找到node_modules
        .pipe(plugins.useref({searchPath: [TEMP, '.', '..']}))
        // 仅在prod模式下进行相应文件压缩
        .pipe(plugins.if(isProd && /\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(isProd && /\.js$/, plugins.uglify()))
        .pipe(plugins.if(isProd && /\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            removeComments: true,
            minifyJS: true,
            minifyCSS: true,
        })))
        .pipe(dest(DIST));
}

image任务

const image = () => {
    return src(PATHS.image, srcConfig)
    // 仅在prod模式下进行压缩
        .pipe(plugins.if(isProd, plugins.imagemin()))
        .pipe(dest(DIST));
}

serve的实现

实现serve时需要考虑以下两点

  1. 开发模式下,监听文件变化,编译调试,需要用到compile任务
  2. 生产模式下,预览production模式打包的项目以获取真实的线上浏览体验,需要用到build任务

serveDev的实现

这是开发环境下的serve,由compile和watcher任务组成

// 开发模式下的serve,编译加watcher即可
const serveDev = series(compile, watcher);

serveProd的实现

这是生产环境下的serve,由compile和watcher任务组成

// prod模式下的serve,需要先打包,然后watcher即可。
// 这时候watcher中不监听任何文件变化,相关逻辑在watcher中处理
const serveProd = series(build, watcher);

在watcher任务中根据isProd处理相关逻辑,代码如下

const watcher = () => {
    // 开发模式下监听文件变化;prod模式不监听
    if (!isProd) {
        // 监听相关css/js/html文件,并重新执行对应的编译
        watch(PATHS.style, srcConfig, style);
        watch(PATHS.script, srcConfig, script);
        watch(PATHS.page, srcConfig, page);
        // 监听图片/字体/其他静态资源文件,刷新浏览器
        watch([PATHS.image, PATHS.font], srcConfig, server.reload);
        watch('**', publicConfig, server.reload);
    }
    const serverCfg = {
        // prod模式只使用dist目录
        // 开发模式下,需使用temp里的css/js/html文件,src下的图片/字体文件,public下的其他静态资源文件
        baseDir: isProd ? [DIST] : [TEMP, SRC, PUBLIC],
        // prod模式下,不需要任何路由;开发模式下,需要通过路由找到/node_modules下的vendor文件
        routes: !isProd && {
            '/node_modules': 'node_modules',
        }
    }
    server.init({
        // 优先使用命令行参数的设置
        port: args['-port'] || 2080,
        open: args['-open'] || false,
        server: {
            ...serverCfg,
        },
    })
}

而最后向外暴露出的serve命令如下

// 对外暴露的serve命令,根据命令行prod参数执行不同的serve
const serve = isProd ? serveProd : serveDev;

deploy的实现

deploy的目标是将打包的dist文件夹git push到github上。这个任务很有意思, 个人思路其实很简单,通过&&连接命令来执行。但中间经过了几轮试错,在这里记录一下过程

round 1. 通过在gulpfile.js里往process.argv里push git相关命令,没有被执行,失败!
function gitPush(branchName = 'gh-pages') {
    //git add foldername
    //git commit -m "commit operation"
    process.argv.push('deploy');
    process.argv.push('&&');
    process.argv.push('git');
    process.argv.push('add');
    process.argv.push('.');
round 2. 我就在想,是不是push argv的时机晚了,于是在bin/index.js里尝试如上方式。报了&&不是一个gulp task,找不到任务,失败!
round 3. 这时候脑子有点转过弯来了,可能跟push参数根本没有关系,需要寻找一个执行命令行的插件
找到两款,分别叫node-cmd和node-run-cmd(Promise式)
先试了node-cmd,中间经过几轮调试,终于成功push了!!!当时bin/index.js代码长这样
const args = require('node-args-parser')(process.argv);
const gulpfilePath = require.resolve('..');
const cmd = require('node-cmd');
const cwd = process.cwd();
process.argv.push('--gulpfile');
process.argv.push(gulpfilePath);
process.argv.push('--cwd');
process.argv.push(cwd);

handleDeploy();
function handleDeploy() {
    // 如果非deploy命令,走require方式执行
    if (process.argv[2] !== 'deploy') {
        require('gulp/bin/gulp');
        return;
    }
    const branchName = args['-branch'] || 'gh-pages';
    console.log(gulpfilePath, cwd);
    // 如果是deploy,为了保证命令的先后执行,需要在这里手动执行gulp
    cmd.run(`gulp deploy --gulpfile ${gulpfilePath} --cwd ${cwd}`, (e, data) => {
        if (e) {
            console.log(e);
            return;
        }
        console.log(data);
        cmd.run('git add .', (e) => {
            if (e) {
                console.log(e);
                ...省略
你以为这就完了?NO!在经过短暂的喜悦之后,我陷入了深深的长考。因为这里的实现有着很明显的问题。
  1. 为了保证git命令在gulp后面执行,自己手动执行了gulp
  2. 回调地狱的代码实在是太TMD丑了!!!
  3. bin的代码太不纯洁了!!!
round 4. 既然这样的方式可行,就想着是不是可以把它放在gulpfile.js里,这不是有node-run-cmd支持Promise嘛,顺便优化一下写法
然而node-run-cmd里面存在一些问题,比如只能打印exitcode,又比如它的Promise会在上一个命令出错的情况下继续往then里走,而不是catch
round 5. 切换回node-cmd,用Promise包装了一下
// 将cmd run方法包装成一个Promise方法exec
const cmd = require('node-cmd');

function exec(line) {
    return new Promise(function (resolve, reject) {
        cmd.run(line, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(data);
        })
    })
}

module.exports = {
    exec,
}
回到gulpfile里写git方法,这下就很好看了,并且能在命令行打印每一步该有的输出
// 使用node-cmd插件异步执行git cmd
const git = (done) => {
    return exec(`git add ./${DIST}`)
        .then((data) => {
            console.log('git add success', data);
            return exec(`git commit -m "commit ${DIST} ${new Date().toLocaleString()}"`)
        })
        .then((data) => {
            console.log('git commit success', data);
            return exec(`git push origin ${args['-branch'] || 'gh-pages'}`)
        })
        .then((data) => {
            console.log('git push success', data);
        })
        .catch(e => {
            console.log(e);
            done(false);
        })
}
deploy命令水到渠成,搞定!
// deploy命令,先打包,再git
const deploy = series(build, git);

整体gulpfile实现思路如上,具体代码及详细注释请参考lib/gulpfile.js

将gulpfile包装为cli

实现如下

1. lib下放置config.js, data.js, gulpfile.js, index.js

config.js和data.js分别为gulpfile的相关配置文件,而index.js只是对gulpfile.js 做了一个导出 依次来看

  • config.js
// 默认的config参数,可以被项目根目录的project.config.js里的config对象覆盖
module.exports = {
    SRC: 'src',
    DIST: 'dist',
    TEMP: 'temp',
    PUBLIC: 'public',
    PATHS: {
        style: 'assets/styles/*.scss',
        script: 'assets/scripts/*.js',
        page: '*.html',
        image: 'assets/images/**',
        font: 'assets/fonts/**',
    }
}
  • data.js
// 默认的data参数,可以被项目根目录的project.config.js里的data对象覆盖
module.exports = {
    menus: [...省略],
    //因为被放在lib下,需要往上一级找到package.json
    pkg: require('../package.json'),
    date: new Date(),
}
  • gulpfile.js
// 获取命令行执行目录
const CWD = process.cwd();

// 设置配置文件名称
const configFileName = 'project.config.js';
// 读取gulp 编译相关默认配置
let cfg = {
    data: require('./data'),
    config: require('./config')
};
try {
    const projectCfg = require(resolve(CWD, configFileName));
    cfg = Object.assign({}, cfg, projectCfg);
} catch (e) {
    console.log(`read ${configFileName} error`);
}
  • index.js 可以被省略
module.exports = require('./gulpfile');

2. 在package.json中添加main,指向lib/index.js

3. 在bin下面创建index.js,代码及说明如下

#!/usr/bin/env node

/**
 * 通过往命令行参数里添加--gulpfile,将运行项目的gulpfile指向当前cli包的lib/gulpfile
 * 通过往命令行参数里添加--cwd,并将运行项目的cwd传入,以防止gulp将运行目录重置为当前cli包目录
 */

process.argv.push('--gulpfile');
// require.resolve仅解析当前文件路径,并不执行当前文件代码,通过入参..的方式找到上一级目录(cli包目录)。
// 此时根据node查找模块的机制,会找到包目录的package.json里的main字段,从而定位到lib下的index文件
process.argv.push(require.resolve('..'));
process.argv.push('--cwd');
process.argv.push(process.cwd());
// console.log(process.argv);
// 这里使用了npm查找包的机制,会找到cli包下的node_modules里的gulp
// 而gulp中执行了require('gulp-cli')(),真正去执行gulpfile.js
require('gulp/bin/gulp');

4. 在package.json中添加bin,指向bin/index.js

5. 在package.json中添加files,声明要导出的bin和lib文件夹

{"files": ["bin","lib"]}

6. 最后yarn publish即可

Readme

Keywords

none

Package Sidebar

Install

npm i sk2-gulp-cli

Weekly Downloads

0

Version

1.0.6

License

MIT

Unpacked Size

38.7 kB

Total Files

14

Last publish

Collaborators

  • caiyue823