Grunt Boilerplate

特别声明:小站对部分原创文章已开启付费阅读,并开通年费VIP通道,年费价格为 ¥365.00元。如果您喜欢小站的内容,可以点击开通会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!(^_^)

本文由大漠根据的《Grunt Boilerplate》所译,整个译文带有我们自己的理解与思想,如果译得不好或不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://www.integralist.co.uk/posts/grunt-boilerplate/,以及作者相关信息

——作者:

——译者:大漠

Grunt是什么?

Grunt是JavaScript创建的任务管理器。这意味着它将帮助你自动化运行任务,比手工运行任务节约更多时间。

Grunt可以预先构建很多繁重的任务,能自动为大多数开发人员提供典型的工作流程。如果你有具体的要求,但并不没有现在提供,你可以编写自己自定义的任务。

大约有700多种使用Grunt创建的任务(截至2013年5月),并且这个任务数还在不断增加。一些很受欢迎的任务,我将在这篇文章中重复介绍。

  • Sass(CSS预处理器)
  • RequireJS(AMD/脚本加载程序)
  • JSHint(JavaScript代码质量)
  • Jasmine BDD(单元测试框架)
  • ImageMin(压缩图像)
  • HTMLMin(压缩HTML)

但还有更多这样的任务:CoffeeScript JavaScript编译,连接文件,连接到一个Web服务器,复制文件和文件夹,handlebar预编译模板,Web浏览器中重新加载,编译代码文档——这里仅列出了其中的一些。

Grunt插件页面,可以看到更多的任务例表(也可以从GitHub中下载)。

安装

Grunt使用Node.js和Node的包管理器系统(NPM)来安装和执行Grunt任务。

原则上安装和运行Grunt(Grunt 0.4)需要有三个项目支持:

  • Node.js
  • NPM
  • Grunt CLI(命令行接口)

在OS X系统中最简单的方法使用Homebrew程序来安装他们(如果你使用的是Windows系统,你需要自己寻找相关安装方法)。

扩展阅读

细节可能不同,但是如果你安装了Homebrew,你可以在你的命令终端执行下面的命令安装Node.js和NPM:

brew install node

下一步是安装Grunt CLI,你可以使用:npm install -g grunt-cli(-g的意思在你的操作系统全局安装Grunt,你可以在你的系统任何地方使用grunt命令)。

现在还想运行:npm install -g grunt-init和有效的安装Grunt的基本要求。

Package.json

在你可以开始使用Grunt之前,你将需要一个package.json文件,用来存储所有的基本配置设置。

你可以执行npm init命令自动生成该文件。

以下是我的Grunt Boilerplate项目中的package.json文件样版:

{
    "name": "Grunt Boilerplate",
    "version": "0.1.0",
    "description": "This is a project set-up using Grunt to take case of some standard tasks such as: compiling AMD based modules using RequireJS, watching/compiling Sass into CSS, watching/linting JS code and some other things such as running unit tests",
    "main": "Gruntfile.js",
    "dependencies": {},
    "devDependencies": {
        "grunt": "~0.4.1",
        "grunt-contrib-watch": "~0.3.1",
        "grunt-contrib-jshint": "~0.4.3",
        "grunt-contrib-uglify": "~0.2.0",
        "grunt-contrib-requirejs": "~0.4.0",
        "grunt-contrib-sass": "~0.3.0",
        "grunt-contrib-imagemin": "~0.1.4",
        "grunt-contrib-htmlmin": "~0.1.3",
        "grunt-contrib-jasmine": "~0.4.2",
        "grunt-template-jasmine-istanbul": "~0.2.1",
        "grunt-template-jasmine-requirejs": "~0.1.1",
        "grunt-contrib-connect": "~0.3.0"
    },
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "repository": {
        "type": "git",
        "url": "git@github.com:Integralist/Grunt-Boilerplate.git"
    },
    "keywords": [
        "Grunt",
        "JavaScript"
    ],
    "author": "Mark McDonnell",
    "license": "MIT"
}

大多数可以运行npm init填允提供的内容和重置(如devDependencies)自动生成的需要安装的Grunt任务(请看下一节)。

依赖性

通常我们不希望依赖我们安装全局的Grunt(记得我们使用-g标记来安装全局的Grunt)。

理由是,依赖你安装项目X可能不同于你的下一个项目。

例如,您可以安装的Grun使用的是1.0版本,在下一个项目中可能已经更新到2.0版本,在他们的API中部分主要功能已更新。所以如果你安装了全局的更新任务,你上次项目不会因为使用旧的API而打破。

但是你仍然想要使用最新和最强的版本。所以你要安装全局的Grunt替代本地的(例如,你目前正在运行的项目安装到特定的项目),这意味项目之间没有相冲突的机会。

下面是我的Grunt Boilerplate项目中安装的Grunt任务:

  • npm install grunt --save-dev
  • npm install grunt-contrib-watch --save-dev
  • npm install grunt-contrib-jshint --save-dev
  • npm install grunt-contrib-uglify --save-dev
  • npm install grunt-contrib-requirejs --save-dev
  • npm install grunt-contrib-sass --save-dev
  • npm install grunt-contrib-imagemin --save-dev
  • npm install grunt-contrib-htmlmin --save-dev
  • npm install grunt-contrib-connect --save-dev
  • npm install grunt-contrib-jasmine --save-dev
  • npm install grunt-template-jasmine-requirejs --save-dev

这里有几件事情要注意:第一你要先安装Grunt(这是Grunt CLI的一部分)。我们已经讨论过了,这意味着我们可以安装我们需要的不同的Grunt版本(根据项目需求)。

另一件事情是使用--save-dev标记,这意味着我们的package.json文件会自动更新,包括依赖我们刚刚安装的。

因为这些依赖荐是安装在本地,你会发现一个新的node_module文件夹出现在您的项目中。这个目录包含上述所有依赖项目/我们刚刚安装的任务。

我建议您创建一个.gitignore文件(您正在使用Git版本控制系统,对吗?),忽略这个文件夹(你不想最终将这些模块提交到你的版本库让其他用户下载,最好让他们按上面的指示安装自己需要的模块)。

如吧,既然我们佛教徒吧安装了依赖关系/任务,让我们看看剩下的一部分,Gruntfile.js

Gruntfile.js

Gruntfile.js主要设置文件和包含为我们已经安装的每个任务的设置。

因为Grunt是使用Node.js运行,你会注意到该文件的内容是包裹在一个闭包中和分配给一个模块的exports属性中。

module.exports = function (grunt) {
    grunt.initConfig({
        // our Grunt task settings
    });
};

函数内我们给Grunt对象起了一个initConfig,我们通过一个对象,在里面设置我们的任务。

第一属性集是我们设置pkg:grunt.file.readJSON('package.json'),这意味着我们项目中可配置文件都在package.json文件中指定。

例如,如果我们想要访问我们包的名称(如果你记得设置是"name":"Grunt Boilerplate"),那么我们可以使用<%= pkg.name %>从内部访问我们的对象。

从这开始,我们开始探索我们之前安装的不同的任务(每个任务的详细设置可以到想的就网站/github仓库中查阅)。

Sass

Sass的任务让我们把Sass文件编译成CSS。

这是一个示例:

sass:{
    dist:{
        options:{
            style:'compressed',
            require:['./assets/styles/sass/helpers/url64.rb']
        },
        expand:true,
        cwd:'./app/styles/sass/',
        src:['*.scss'],
        dest:'./app/styles/',
        ext:'.css'
    },
    dev:{
        options:{
            style:'expanded',
            debugInfo:true,
            lineNumbers:true,
            require:['./app/styles/sass/helpers/url64.rb']
        },
        expand:true,
        cwd:'./app/styles/sass',
        src:['*.scss'],
        dest:'./app/styles/',
        ext:'.css'
    }
}

你在上面的例子中可以看到,我们设置了两个子任务distdev

设置两个子任务是我想当我在开发我的应用时Sass文件可以编译到CSS(包括debug信息),但当我完成我的项目时,我想将我的Sass文件编译出的CSS文件能够压缩直接引入项目中。

你会看到每个子任我指定了一个options对象,告诉编译器如何编译Sass以及在哪里可以找到Sass的帮助脚本。

你会发现在我的Grunt Boilerplate(上面使用的示例中)包含了Sass的帮助脚本require:['./assets/styles/sass/helpers/url64.rb'],它允许您使用一个Sass的特殊函数,将一个背景图片转换成Base64编码的这符串,这样可以更好的减少一个对图像的HTTP请求,提高性能。它也适用于IE8的Base64编码字符串,但有一个问题存在,转换出来的数据不能大于32kb。

最后,我们使用一个Grunt特定的模式,允许我们更好的得到目标和输出多个文件:expand:true。有效的设置允许其他属性遵循它被激活(如果我们没有设置expand:true,属性不会正常工作)。

让我们来看看其他有点接近的属性,你会看到其他任务也会使用类似的方式,所以重要的是要了解它们是如何工作的。

cwd: './app/styles/sass/':在这里我设置了当前工作目录(这是我想要Grunt找到我的Sass文件)。

src: ['*.scss']:这里告诉Grunt,我想在我当前工作目录中寻找任何.scss文件。

dest: './app/styles/':这里告诉Grunt,我我答应你它导出的文件放到这个目录。

ext: '.css':我希望每个导出的文件具有一个.css的扩展名。

要运行这个特定的任务,我们可以打开我们的终端,执行:grunt sass(这样将执行两个子任务),或者我们可以执行指定的子任务,像这样:grunt sass:dev

在这里我不会解释运行特定任务,因为所有任务执行都是相同的。

扩展阅读

RequireJS

这个任务是来自于RequireJS的一个有效的r.js构建脚本,因此我不想详细介绍如何构建这个脚本,相反我会告诉你r.js文档,但要知道在使用r.js之前,你要完成像下面的设置。

requirejs:{
    compile:{
        options:{
            baseUrl:'./app',
            mainConfigFile:'./app/main.js',
            dir:'./app/release',
            fileExclusionRegExp:/^\.|node_modules|Gruntfile|\.md|package.json/,
            modules:[
                {
                    name:'main'
                }
            ]
        }
    }
}

扩展阅读

JSHint

JSHint是Grunt的一个任务,你可以点击这里,你可以链接到指定文件的头部。(他是用来确保你的代码根据一组你希望的规则编写代码,让代码语法更有效)。

jshint:{
    files:['Gruntfile.js','app/**/*.js','!app/release/**','modules/**/*.js','specs/**/*Spec.js'],
    options:{   
        curly:true,//如果为真,JSHint会要求你在使用if和while等结构语句时加上{}来明确代码块。        
        eqeqeq:true,//如果为真,JSHint会看你在代码中是否都用了===或者是!==,而不是使用==和!。
        immed:true,////如果为真,JSHint要求匿名函数的调用如下:(function(){//}());而不是(function(){}(//bla bla));
        latedef:true,
        newcap:true,//JSHint会要求每一个构造函数名都要大写字母开头。
        noarg:true,//如果为真,JSHint会禁止arguments.caller和arguments.callee的使用
        sub:true,//如果为真,JSHint会允许各种形式的下标来访问对象。
        undef:true,//如果为真,JSHint会要求所有的非全局变量,在使用前都被声明。
        boss:true,//如果为真,那么JSHint会允许在if,for,while里面编写赋值语句。
        eqnull:true,//JSHint会允许使用"== null"作比较
        browser:true,

        globals:{
            //AMD
            module: true,
            require:true,
            requirejs: true,
            define: true,

            //Environments
            console: true,

            //General Purpose Libraries
            $: true,
            jQuery:true,

            //Testing
            sinon: true,
            describe:true,
            it: true,
            expect:true,
            beforeEach: true,
            afterEach: true
        }
    }
}

有些事情需要注意:我们使用了重点操作符!,它是用来告诉Grunt忽略特定的目录。因此你可以看到我们的设置!app/release/,这意味着忽略指定的目录,因为它可能会根据JSHint的规则集导致错误(因为我们的目录是放置通过RequireJS编译出来的简化版本的JavaScript代码)。

同时你将会看到我们已经告诉JSHint根据我们预想的东西设置了某些变量(例如全局可用变量)。我们需要这样做,因为JSHint是单独的检查我们的JavaScript文件,当我们的应用程序加载所有脚本在一起时,JSHint不会自动识别这些不同的全局变量,所以,如果我们没有告诉它,将会出错。

最后,有关于JSHint规则的完整列表,可以点击JSHint官网查阅。

扩展阅读

Jasmine BDD

Jasmine是一个单元测试框架和声明库。

这可能是一个最复杂的任务,因为它需要讨论一些其他的任务。

首要的任务它依赖于connect任务,而这个任务启动一个使用PhantomJS的Web服务器。但是这个不要求你的脚本是否有DOM(有可能是你不做Web开发),但我们做了DOM操作以及我们需要一个DOM交互测试做对比,因此需要一个connect连接服务器的任务。

connect:{
    test:{
        port:8000
    }
}

接着,我们来设置实现jasmine的任务。

jasmine:{
    src:['app/**/*.js','!app/release/**'],
    options:{
        host:'http://127.0.0.1:8000/',
        specs:'specs/**/*Spec.js',
        helpers:['specs/helpers/*Helper.js','specs/helpers/sinon.js'],
        template:require('grunt-template-jasmine-requirejs'),
        templateOptions: {
            requireConfig:{
                baseUrl:'./app/',
                mainConfigFile:'./app/main.js'
            }
        }
    }
}

你会看到我们指定了一个与connectWeb服务器任务有关的host参数。

我们设置了specs参数,告诉Grunt在哪找到BDD规范和测试文件。

我们设置了一个helpers参数,用来加载做单元测试所需要的额外脚本。(这里额外加载了一个叫Sinon.js的脚本,这个脚本帮助我们在单元测试中做Handles Spies,Mocks和Stubs测试)。

接下来是复杂任务中的其他一部分:我们需要一个template的子任务,这个子任务是专门为Jasmine创建的:grunt-template-jasmine-requirejs。我们需要这些额外的任务,因为我们使用的是AMD让我们的JavaScript代码模块化,但AMD介绍中谈到了基于动态运行中的异步加载有一个问题,如Grunt jasmin任务。

你会看到,我们只需要告诉grunt-template-jasmine-requirejs,基于JS的目录是依靠AMD的主要文件来引导我们的应用程序。在后台会生成一个_SpecRunner.html文件,基本上需要的每个单的AMD模块都在里面,只要开始测试就会回调所有的模块。这并不是最好的解决方案,但它是可以正常工作的。

如果测试通过了就会删除_SpecRunner.html文件,所以你永远不会注意到它,但如果有任何测试失败,测试文件会保存在你的根目录中,这样你就可以检查文件和通过一个真正的Web浏览器手动运行这个文件,尝试和调试任何失败的测试。

扩展阅读

图片压缩

ImageMin任务正像您所想象的,它会搜索出任何图像,并找出(png或jpg格式)和压缩他们成较小的文件大小。

正如你从下面的例子中所看到的,我们使用了expand:true设置,他告诉Grunt从哪找到我们的图片和将他们导出到哪里。

imagemin: {
    png: {
        options: {
            optimizationLevel:7
        },
        files:[
            {
                expand:true,
                cwd:'./app/images',
                src:['**/*.png'],
                dest:'./app/images/compressed',
                ext:'.png'
            }
        ]
    },
    jpg: {
        options:{
            progressive: true
        },
        files: [
            {
                expand: true,
                cwd:'./app/images',
                src:['**/*.jpg'],
                dest:'./app/images/compressed',
                ext:'.jpg'
            }
        ]
    }
}

扩展阅读

HTML压缩

HTMLMin任务是搜索出任何HTML文件,找到并压缩他们,经过压缩后他们是较小的文件。

正如下从下面的例子中看到的一样,我们没有设置expand:true,而这一任务提供了一个稍为不同的API,告诉Grunt在哪里找到我们的HTML文件,并导出压缩版本(但实际上,如果我们需要,可以设置expand:true)。

htmlmin: {
    dist:{
        options: {
            removeComments: true,
            collapseWhitespace: true,
            removeEmptyAttributes: true,
            removeCommentsFromCDATA:true,
            removeRedundantAttributes: true,
            collapseBooleanAttributes:true
        },
        files: {
            //Destination: Source
            './index-min.html':'./index.html'
        }
    }
}

注册任务

到目前为止,我们已经看到了可以像grunt sass:devgrunt jshint运行指定的Grunt任务,但你也可以设置一个自定义的任务,他不做任何事情,但运行其他任务。

例如,你可以创建一个任务,将执行一个特定的任务集。

grunt.registerTask('release', ['jshint', 'jasmine', 'requirejs', 'sass:dist', 'imagemin', 'htmlmin']);

在上面的示例中,我们已经创建了一个release任务,运行时将把我们准备好的文件提交到我们的生产环境。我通常运行这个任务当作我完成了我的应用创建。

正如你所看到的,我的JavaScript文件不零乱,然后确保我的JavaScript通过测试。接下来是构建RequireJS脚本,然后是我的dist中的Sass任务(一个专门为我们生产服务器,所以它压缩所有编译的CSS文件),最后是压缩好的图片和HTML文件。

当你在命令行中执行grunt命令本身时,Grunt就会寻找一个已注册的任务,叫作default

grunt.registerTask('default', ['jshint', 'connect', 'jasmine', 'sass:dev']);

在这种情况下,我们要告诉它要执行JavaScript的jshint任务,然后检查我们的单元测试和最后生成调试版本的CSS。

扩展阅读

监测文件

每次手动的代替运行Grunt命令,我们需要做出改变(例如,想象一下改变你的Sass文件,然后在命令终端运行Grunt命令sass:dev获得Sass编译出来的CSS,你能在浏览器中看到其中的变化。),我们可以使用Grunt为我们努力工作和自动运行一个任务(或多个任务),指定的文件都已改变/更新。

watch: {
    files: ['<%= jshint.files %>', '<%= jasmine.options.specs %>', '<%= sass.dev.src %>'],
    tasks: 'default'
}

在这里你可以看到我们使用watch单独任务,以及具体的指定语法让它知道要监控文件。

语法是:<%= jshint.files %>,在这种情况下它的意思是“查看jshint属性和将返回值设置其子属性files”。

你从上面的例子中你可以看到,如果运行了default注册任务,告诉watch任务找到和修改JavaScript和Sass文件。

整个Grunt文件

你可以在我的Grunt Boilerplate项目中找到以下文件:

module.exports = function (grunt) {

    /*
        Grunt installation:
        -------------------
            npm install -g grunt-cli
            npm install -g grunt-init
            npm init (creates a `package.json` file)

        Project Dependencies:
        ---------------------
            npm install grunt --save-dev
            npm install grunt-contrib-watch --save-dev
            npm install grunt-contrib-jshint --save-dev
            npm install grunt-contrib-uglify --save-dev
            npm install grunt-contrib-requirejs --save-dev
            npm install grunt-contrib-sass --save-dev
            npm install grunt-contrib-imagemin --save-dev
            npm install grunt-contrib-htmlmin --save-dev
            npm install grunt-contrib-connect --save-dev
            npm install grunt-contrib-jasmine --save-dev
            npm install grunt-template-jasmine-requirejs --save-dev
    */

    // Project configuration.
    grunt.initConfig({

        // Store your Package file so you can reference its specific data whenever necessary
        pkg: grunt.file.readJSON('package.json'),

        // Used to connect to a locally running web server (so Jasmine can test against a DOM)
        connect: {
            test: {
                port: 8000
            }
        },

        jasmine: {
            /*
                Note:
                In case there is a /release/ directory found, we don't want to run tests on that 
                so we use the ! (bang) operator to ignore the specified directory
            */
            src: ['app/**/*.js', '!app/release/**'],
            options: {
                host: 'http://127.0.0.1:8000/',
                specs: 'specs/**/*Spec.js',
                helpers: ['specs/helpers/*Helper.js', 'specs/helpers/sinon.js'],
                template: require('grunt-template-jasmine-requirejs'),
                templateOptions: {
                    requireConfig: {
                        baseUrl: './app/',
                        mainConfigFile: './app/main.js'
                    }
                }
            }
        },

        jshint: {
            /*
                Note:
                In case there is a /release/ directory found, we don't want to lint that 
                so we use the ! (bang) operator to ignore the specified directory
            */
            files: ['Gruntfile.js', 'app/**/*.js', '!app/release/**', 'modules/**/*.js', 'specs/**/*Spec.js'],
            options: {
                curly:   true,
                eqeqeq:  true,
                immed:   true,
                latedef: true,
                newcap:  true,
                noarg:   true,
                sub:     true,
                undef:   true,
                boss:    true,
                eqnull:  true,
                browser: true,

                globals: {
                    // AMD
                    module:     true,
                    require:    true,
                    requirejs:  true,
                    define:     true,

                    // Environments
                    console:    true,

                    // General Purpose Libraries
                    $:          true,
                    jQuery:     true,

                    // Testing
                    sinon:      true,
                    describe:   true,
                    it:         true,
                    expect:     true,
                    beforeEach: true,
                    afterEach:  true
                }
            }
        },

        requirejs: {
            compile: {
                options: {
                    baseUrl: './app',
                    mainConfigFile: './app/main.js',
                    dir: './app/release/',
                    fileExclusionRegExp: /^\.|node_modules|Gruntfile|\.md|package.json/,
                    // optimize: 'none',
                    modules: [
                        {
                            name: 'main'
                            // include: ['module'],
                            // exclude: ['module']
                        }
                    ]
                }
            }
        },

        sass: {
            dist: {
                options: {
                    style: 'compressed',
                    require: ['./assets/styles/sass/helpers/url64.rb']
                },
                expand: true,
                cwd: './app/styles/sass/',
                src: ['*.scss'],
                dest: './app/styles/',
                ext: '.css'
            },
            dev: {
                options: {
                    style: 'expanded',
                    debugInfo: true,
                    lineNumbers: true,
                    require: ['./app/styles/sass/helpers/url64.rb']
                },
                expand: true,
                cwd: './app/styles/sass/',
                src: ['*.scss'],
                dest: './app/styles/',
                ext: '.css'
            }
        },

        // `optimizationLevel` is only applied to PNG files (not JPG)
        imagemin: {
            png: {
                options: {
                    optimizationLevel: 7
                },
                files: [
                    {
                        expand: true,
                        cwd: './app/images/',
                        src: ['**/*.png'],
                        dest: './app/images/compressed/',
                        ext: '.png'
                    }
                ]
            },
            jpg: {
                options: {
                    progressive: true
                },
                files: [
                    {
                        expand: true,
                        cwd: './app/images/',
                        src: ['**/*.jpg'],
                        dest: './app/images/compressed/',
                        ext: '.jpg'
                    }
                ]
            }
        },

        htmlmin: {
            dist: {
                options: {
                    removeComments: true,
                    collapseWhitespace: true,
                    removeEmptyAttributes: true,
                    removeCommentsFromCDATA: true,
                    removeRedundantAttributes: true,
                    collapseBooleanAttributes: true
                },
                files: {
                    // Destination : Source
                    './index-min.html': './index.html'
                }
            }
        },

        // Run: `grunt watch` from command line for this section to take effect
        watch: {
            files: ['<%= jshint.files %>', '<%= jasmine.options.specs %>', '<%= sass.dev.src %>'],
            tasks: 'default'
        }

    });

    // Load NPM Tasks
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-requirejs');
    grunt.loadNpmTasks('grunt-contrib-sass');
    grunt.loadNpmTasks('grunt-contrib-imagemin');
    grunt.loadNpmTasks('grunt-contrib-htmlmin');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-jasmine');

    // Default Task
    grunt.registerTask('default', ['jshint', 'connect', 'jasmine', 'sass:dev']);

    // Unit Testing Task
    grunt.registerTask('test', ['connect', 'jasmine']);

    // Release Task
    grunt.registerTask('release', ['jshint', 'jasmine', 'requirejs', 'sass:dist', 'imagemin', 'htmlmin']);

};

结论

希望本指南能给你一个开始使用Grunt自动化工作流程的很好起点。这是一个非常强大的工具,甚至开始没有任何内置的自定义任务,你可以写自己的任务与文件系统,你可以做任何你想要做的事情。

注意:我的Grunt Boilerplate项目是不断更新的,可以时刻观注我们的版本库。

译者手语:整个翻译依照原文线路进行,并在翻译过程略加了个人对技术的理解。如果翻译有不对之处,还烦请同行朋友指点。谢谢!

如需转载,烦请注明出处:

英文原文:http://www.integralist.co.uk/posts/grunt-boilerplate/

中文译文:http://www.w3cplus.com/tools/grunt-boilerplate.html

Nike Zoom Vomero 14

如需转载,烦请注明出处:https://www.w3cplus.com/tools/grunt-boilerplate.html

如果文章中有不对之处,烦请各位大神拍正。如果你觉得这篇文章对你有所帮助,打个赏,让我有更大的动力去创作。(^_^)。看完了?还不过瘾?点击向作者提问!

赏杯咖啡,鼓励他创作更多优质内容!
返回顶部