# sk2-gulp-cli **Repository Path**: caiyue823/sk2-gulp-cli ## Basic Information - **Project Name**: sk2-gulp-cli - **Description**: gulp cli - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-06-14 - **Last Updated**: 2021-08-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # sk2-gulp-cli [项目主页](https://gitee.com/caiyue823/sk2-gulp-cli) ## 介绍 一个内置gulp-cli & gulpfile & 灵活配置的工作流cli 模板文件脚手架项目见[sk2-gulp-pages](https://gitee.com/caiyue823/sk2-gulp-pages) ## 项目结构 ``` └── 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 -D` 或 `yarn 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,可以直接复制它到你的项目根目录进行修改即可。 ```javascript 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的实现 ```javascript 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 ```javascript const lint = parallel(styleLint, scriptLint); ``` 在实现lint命令时使用parallel并行执行,一是缩短时间,二是因为任何一项lint任务一旦报错都无需往下继续。 当然这样做也有一个缺点,子任务的报错日志会交替输出,阅读上有点麻烦。 #### compile的实现 compile任务分为css/js/html三个子任务,输出半成品的css/js/html文件,配合useref/watcher任务使用。 以js任务为例,实现如下 ```javascript // 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任务采用并行执行,以提高效率 ```javascript // 编译,并行执行 const compile = parallel(style, script, page); ``` #### build的实现 build任务的实现如下 ```javascript // 打包命令,需要先clean,然后并行执行(css/js/html的编译与引用查找,以及其他静态资源的压缩) const build = series(clean, parallel(series(compile, useref), image, font, extra)); ``` 由于build需要区分开发/生产模式,因此在useref/image/font等子任务中均需要做相应处理(不包括compile)。 以下代码通过插件从命令行中获取isProd。当isProd为true时, 需要对所有的资源进行压缩/混淆/查找引用等处理 ```javascript // 转化命令行参数为一个对象 const args = require('node-args-parser')(process.argv); console.log('args', args); // 处理相关命令行参数 // 提取prod参数供后续使用 const isProd = args['-production'] || args['-prod'] || false; ``` 其中useref的实现如下 ```javascript 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任务 ```javascript 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任务组成 ```javascript // 开发模式下的serve,编译加watcher即可 const serveDev = series(compile, watcher); ``` #### serveProd的实现 这是生产环境下的serve,由compile和watcher任务组成 ```javascript // prod模式下的serve,需要先打包,然后watcher即可。 // 这时候watcher中不监听任何文件变化,相关逻辑在watcher中处理 const serveProd = series(build, watcher); ``` 在watcher任务中根据isProd处理相关逻辑,代码如下 ```javascript 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命令如下 ```javascript // 对外暴露的serve命令,根据命令行prod参数执行不同的serve const serve = isProd ? serveProd : serveDev; ``` ### deploy的实现 deploy的目标是将打包的dist文件夹git push到github上。这个任务很有意思, 个人思路其实很简单,通过&&连接命令来执行。但中间经过了几轮试错,在这里记录一下过程 ##### round 1. 通过在gulpfile.js里往process.argv里push git相关命令,没有被执行,失败! ```javascript 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代码长这样 ```javascript 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包装了一下 ```javascript // 将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方法,这下就很好看了,并且能在命令行打印每一步该有的输出 ```javascript // 使用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命令水到渠成,搞定! ```javascript // 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 ```javascript // 默认的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 ```javascript // 默认的data参数,可以被项目根目录的project.config.js里的data对象覆盖 module.exports = { menus: [...省略], //因为被放在lib下,需要往上一级找到package.json pkg: require('../package.json'), date: new Date(), } ``` * gulpfile.js ```javascript // 获取命令行执行目录 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 可以被省略 ```javascript module.exports = require('./gulpfile'); ``` #### 2. 在package.json中添加main,指向lib/index.js #### 3. 在bin下面创建index.js,代码及说明如下 ```javascript #!/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文件夹 ```json {"files": ["bin","lib"]} ``` #### 6. 最后yarn publish即可