使用YEOMAN创建属于自己的前端工作流

a158318 9年前
   <h2>背景</h2>    <p>前几天看了一篇文章大受启发:我理想中的前端工作流,现在工作中一直在使用gulp和webpack做自动化,在单独项目中效果很不错。但是随着项目逐渐怎多,是需要使用一个工具来帮你快速创建和规范项目。</p>    <p>Yeoman是什么</p>    <p>Yeoman是一个脚手架,可以快速生成一个项目的骨架。官网上有很多大家 <a href="/misc/goto?guid=4959664198984160869" rel="nofollow,noindex">已经写好的脚手架</a> ,也可以自己写一个适合自己的,接下来我会翻译下官网的教程,学习的同时把经验分享给大家。</p>    <p>Yeoman能做什么</p>    <p>Yeoman只是帮我们生成项目的骨架是远远不够的,官网上介绍,Yeoman是由三部分组成的:脚手架工具 - Yo、构建工具 - Grunt or Gulp、包管理工具 - Bower or npm。</p>    <h2>创建Yeoman</h2>    <p><a href="/misc/goto?guid=4959640899054194805" rel="nofollow,noindex">翻译yeoman官网Creating a generator流程</a></p>    <p>(一)Getting started 快速开始</p>    <p>(二)Running Context 生命周期</p>    <p>(三)User Interactions 和使用者互动</p>    <p>(四)composability 组合</p>    <p>(五)Managing Dependencies 依赖管理</p>    <p>(六)Interacting with the file system 文件操作</p>    <h3>(一)快速开始</h3>    <p><a href="/misc/goto?guid=4959640899054194805" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <p>1.1 建立一个node模块</p>    <p>1.首先要建立一个文件夹,在这个文件夹内写你的generator,这个文件夹的名字 <strong>必须</strong> 被命名为 generator-name ,name为你generator的名字,假如我想写一个vue的脚手架,我可以命名为: generator-vue ,这个很关键,Yeoman文件系统只会信任这种规范的generator。</p>    <pre>  <code class="language-javascript">mkdir generator-vue</code></pre>    <p>2.建立node模块,首先要必备文件 package.json ,这个文件可以通过执行 npm init 指令来生成,前提是需要安装 node 及 npm。</p>    <pre>  <code class="language-javascript">{    "name": "generator-vue",    "version": "0.1.0",    "description": "",    "files": [      "app",      "router"    ],    "keywords": ["yeoman-generator"],    "dependencies": {      "yeoman-generator": "^0.20.2"    }  }</code></pre>    <p>几点要求:</p>    <ol>     <li> <p>name:必须格式为 generator-你项目的名字 。</p> </li>     <li> <p>keywords:数组中必须有 yeoman-generator ,这样你的项目才会被 <a href="/misc/goto?guid=4959664198984160869" rel="nofollow,noindex">Yeoman官方的generators列表</a> 所收录。</p> </li>     <li> <p>如果需要添加其他的属性可以去 <a href="/misc/goto?guid=4959670077297976152" rel="nofollow,noindex">npm官网文档</a> 中查看。</p> </li>    </ol>    <p>1.2 文档结构</p>    <p>通过1.1步骤已经有了package.json,下一步在新建两个文件夹分别叫app和router,结构如下。</p>    <pre>  <code class="language-javascript">├───package.json  ├───app/  │   └───index.js  └───router/      └───index.js</code></pre>    <p>1.2.1 默认项目</p>    <p>当你执行Yeoman指令 yo vue (上面你已经建立的项目名字)的时候,他会默认执行你根目录下app/index.js的内容,所以一个新项目, app/ 目录是必须的。</p>    <p>1.2.2 子项目</p>    <p>router/ 作为子项目,可以通过 yo vue:router 来执行。</p>    <p>1.2.3 更改文件目录</p>    <pre>  <code class="language-javascript">├───package.json  └───generators/      ├───app/      │   └───index.js      └───router/          └───index.js</code></pre>    <p>如果不喜欢把所有项目都放在根目录下,Yeoman还允许把项目放在 generators/ 下面,改写上面的例子:</p>    <pre>  <code class="language-javascript">├───package.json  └───generators/      ├───app/      │   └───index.js      └───router/          └───index.js</code></pre>    <p>如果更改了文件目录,要同步修改 package.json 中对应文件的目录结构:</p>    <pre>  <code class="language-javascript">{    "files": [      "generators/app",      "generators/router"    ]  }</code></pre>    <p>1.3 扩展generator</p>    <p>1.3.1 重写构造函数</p>    <pre>  <code class="language-javascript">module.exports = generators.Base.extend({    // The name `constructor` is important here    constructor: function () {      // Calling the super constructor is important so our generator is correctly set up      generators.Base.apply(this, arguments);        // Next, add your custom code      this.option('coffee'); // This method adds support for a `--coffee` flag    }  });</code></pre>    <p>1.3.2 添加自己的方法</p>    <pre>  <code class="language-javascript">module.exports = generators.Base.extend({    method1: function () {      console.log('method 1 just ran');    },    method2: function () {      console.log('method 2 just ran');    }  });</code></pre>    <p>下一步的时候当你运行generator的时候,会看到这两句console输出在控制台。</p>    <p>1.4 运行generator</p>    <p>1.4.1 安装全局generator</p>    <p>在项目跟路径下 generator-name(vue)/ 执行指令:</p>    <pre>  <code class="language-javascript">npm link</code></pre>    <p>过程中,将会安装node模块依赖,和创建软连接指向你当前项目。</p>    <pre>  <code class="language-javascript">//1.到本地全局node模块路径下  cd /usr/local/lib/node_modules     //2.查看列表  ll     //3.会看到已经安装了一个全局的geneator-vue模块  npm  geneator-vue -> /Users/lvjinlong/generator-vue  gulp  ..      //4.此时在任意新建的项目文件夹中yo项目的名字,会看到上面实例中的console打印出来的结果。  yo vue</code></pre>    <p>1.4.2 寻找根目录</p>    <p>安装geneator的时候,Yeoman会搜索你的文件夹,会把包含 .yo-rc.json 文件的文件夹作为你的根目录来初始化项目。</p>    <p>问:那么, .yo-rc.json 是个什么东西呢?</p>    <p>答:当你 <strong>第一次</strong> 调用 this.config.save() 的时候,系统会生成这个文件。</p>    <p>问:那么, this.config.save() 这个方法的作用是什么呢?</p>    <p>答: <a href="/misc/goto?guid=4959670077396211959" rel="nofollow,noindex">官网</a> 这篇文章会有讲解,大体意思是会利用 .yo-rc.json 来存储或是读取用户相关信息。</p>    <p>所以当你初始化一个项目的时候,别忘记清除掉之前系统生成的 .yo-rc.json 。</p>    <h3>(二)生命周期</h3>    <p><a href="/misc/goto?guid=4959670077475836049" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <p>2.1 Prototype methods as actions</p>    <p>每个方法会直接附加在generator原型上作为一个action,每个action按照一定的循序执行在Yeoman的生命周期中。</p>    <p>这些方法相当于直接执行了 Object.getPrototypeOf(generator) ,</p>    <p>所有方法都会 <strong>自动执行</strong> 。如果不想都自动执行,请往下看。</p>    <p>2.1.1 私有方法</p>    <p>只有私有方法在Yeoman中才不会自动执行,下面有三种办法帮你创建一个私有方法。</p>    <p>1. 在方法名前面加下划线(例如: _method )</p>    <p>2. 使用实例方法</p>    <pre>  <code class="language-javascript">generators.Base.extend({    init: function () {      this.helperMethod = function () {        console.log('won\'t be called automatically');      };    };  });</code></pre>    <p>3.继承父generator</p>    <pre>  <code class="language-javascript">var MyBase = generators.Base.extend({      helper: function () {        console.log('won\'t be called automatically');      }    });      module.exports = MyBase.extend({      exec: function () {        this.helper();      }    });</code></pre>    <p>2.2 生命周期</p>    <p>Yeoman中的定义了生命周期钩子,和这些钩子命名相同的会按照顺序执行,如果和这些钩子名称不一样则默认为 default</p>    <p>这些生命周期钩子 <strong>按顺</strong> 序为:</p>    <ol>     <li> <p>initializing:初始化方法(检验当前项目状态、获取configs、等)</p> </li>     <li> <p>prompting:获取用户选项</p> </li>     <li> <p>configuring:保存配置(创建 .editorconfig 文件)</p> </li>     <li> <p>default:如果函数名称如生命周期钩子不一样,则会被放进这个组</p> </li>     <li> <p>writing:写generator特殊的文件(路由、控制器、等)</p> </li>     <li> <p>conflicts:冲突后处理办法</p> </li>     <li> <p>install:正在安装(npm、bower)</p> </li>     <li> <p>end:安装结束、清除文件、设置good bye文案、等</p> </li>    </ol>    <h3>(三)和用户互动</h3>    <p><a href="/misc/goto?guid=4959670077560652879" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <p>Yeoman默认在终端中执行,但是也支持在多种不同工具中执行。这时候我们使用 console.log() 或是 process.stdout.write() 用户就可能看不到,Yeoman中使用 generator.log() 来统一打印输出结果。</p>    <p>3.1和用户互动</p>    <p>3.1.1 Prompts - 提示框</p>    <p>提示框是Yeoman主要和用户交流的手段,是通过 <a href="/misc/goto?guid=4959615058602740111" rel="nofollow,noindex">Inquirer</a> 模块来实现的,所有的API及参数可以看 <a href="/misc/goto?guid=4959615058602740111" rel="nofollow,noindex">这里</a> ,执行以下实例看下效果:</p>    <pre>  <code class="language-javascript">module.exports = generators.Base.extend({    prompting: function () {      var done = this.async();      this.prompt({        type    : 'input',        name    : 'name',        message : 'Your project name',        default : this.appname // Default to current folder name      }, function (answers) {        this.log(answers.name);        done();      }.bind(this));    }  })</code></pre>    <p>3.1.2 Remembering user preferences 记录用户预设参数</p>    <p>一个确定的答案,比如帐号,用户可能多次提交同一个答案,这时候可以用Yeoman提供 store 来存储这些答案。</p>    <pre>  <code class="language-javascript">this.prompt({    type    : 'input',    name    : 'username',    message : 'What\'s your Github username',    store   : true  }, callback);</code></pre>    <p>这时候会在跟路径下生成一个 .yo-rc.json 文件,里面会存储name信息。 <a href="/misc/goto?guid=4959670077396211959" rel="nofollow,noindex">可以参考官网storage这一节</a></p>    <p>3.1.3 Arguments - 参数</p>    <p>参数直接通过命令行传递,例如:</p>    <pre>  <code class="language-javascript">yo webapp my-project</code></pre>    <p>这个例子中,my-project 是第一个参数。</p>    <p>通知系统我们需要参数,我们使用 generator.argument() 方法,这个方法接受两种形式:</p>    <ol>     <li> <p>name(String) -- generator['name']</p> </li>     <li> <p>hash(key-value) -- 哈希值的形式,接受以下参数作为key值</p>      <ul>       <li> <p>desc -> 参数描述</p> </li>       <li> <p>required -> 是否为必须传递 [ ture | false ]</p> </li>       <li> <p>optional -> 是否可选 [ ture | false ]</p> </li>       <li> <p>type -> 参数类型 [ String | Number | Array | Object]</p> </li>      </ul> </li>    </ol>    <pre>  <code class="language-javascript">var _ = require('lodash'); //需要提前安装lodash模块,提供一些常用方法  module.exports = generators.Base.extend({    //注: arguments和options必须在constructor中定义.    constructor: function () {      generators.Base.apply(this, arguments);      //appname为一个必须的参数      this.argument('appname', { type: String, required: true });      //用驼峰式把这个参数保存起来      this.appname = _.camelCase(this.appname);    }  });</code></pre>    <p>3.1.4 Options - 选项</p>    <p>Options(选项)看起来像是Arguments(参数),但是他们是在命令行中的标志。</p>    <p>实例:举一个官网团队的脚手架demo - <a href="/misc/goto?guid=4959670077698642310" rel="nofollow,noindex">webapp - 15行</a></p>    <pre>  <code class="language-javascript">module.exports = generators.Base.extend({    constructor: function () {      generators.Base.apply(this, arguments);      this.option('skip-welcome-message', {        desc: 'Skips the welcome message',        type: Boolean      });    }  })</code></pre>    <p>用法:</p>    <p><a href="/misc/goto?guid=4959670077788710689" rel="nofollow,noindex">webapp - options</a></p>    <pre>  <code class="language-javascript">yo webapp --skip-install</code></pre>    <p>3.2 输出信息</p>    <p>输出信息使用 generator.log 模块,和js的 console.log() 基本一致。</p>    <pre>  <code class="language-javascript">module.exports = generators.Base.extend({    myAction: function () {      this.log('Something has gone wrong!');    }  });</code></pre>    <p>传值的方式同Arguments(参数),字符串或hash。区别是参数:</p>    <ul>     <li> <p>desc:描述</p> </li>     <li> <p>alias:简写(--version 简写为 -v)</p> </li>     <li> <p>type:[ Boolean | String | Number ]</p> </li>     <li> <p>defaults:默认值</p> </li>     <li> <p>hide :[ Boolean ] 是否隐藏帮助信息</p> </li>    </ul>    <h2>(四)组合</h2>    <p><a href="/misc/goto?guid=4959670077873485371" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <p>很有趣的是,官网的第一个demo竟然是一个变形金刚组合的 <a href="https://simg.open-open.com/show/bedb57804c35feeeb687b5af777cd81a.gif" rel="nofollow,noindex">gif</a> ,可见他们是多么想表达各个小功能组合起来后的yeoman是有多强大。</p>    <p>可以通过以下两种方式开始组合:</p>    <ol>     <li> <p>依赖另外一个generator(例如: generator-backbone 使用 generator-mocha )。</p> </li>     <li> <p>使用者,根据自己的需求在初始化项目的时候选择配置。(例如: sass 或者 less 来搭配 webpack 或是 gulp )</p> </li>    </ol>    <h3>4.1 generator.composeWith()</h3>    <p>composeWith 方法允许你的generator来组合别人的generator,但是一旦组合成功,不要忘记第二章的内容 <strong><(二)Running Context 生命周期></strong> ,所有被组合的generator都遵循Yeoman的生命周期规则来顺序执行,不同的generator执行顺序,取决于 composeWith 调用他们的顺序,看下面的API及执行顺序实例。</p>    <p>4.1.1 API</p>    <p>composeWith 接收三个参数:</p>    <ol>     <li> <p>namespace :声明generator和谁组合。[ String ]</p> </li>     <li> <p>options :调用generator的时候需要接收的参数。[ Object | Array ]</p> </li>     <li> <p>settings :你的generator用这些配置来决定如果运行其他的generators。[ Object ]</p>      <ul>       <li> <p>settings.local :需要在 dependencies 中配置,使用dependencies安装的模块相当于本地模块,这里使用 require.resolve 来返回一个本地模块的路径,如: node_modules/generator-name [ String ]</p> </li>       <li> <p>settings.link : weak or strong [ String ]</p>        <ul>         <li> <p>week link:在初始化的时候不运行,比如后端运行的,frameworks或css的预处理。</p> </li>         <li> <p>strong link:一直运行。</p> </li>        </ul> </li>      </ul> </li>    </ol>    <p>当需要用 peerDependencies 来组合generator</p>    <pre>  <code class="language-javascript">this.composeWith('backbone:route', { options: {    rjs: true  }});</code></pre>    <p>当需要用 dependencies 来组合generator</p>    <pre>  <code class="language-javascript">this.composeWith('backbone:route', {}, {    local: require.resolve('generator-bootstrap')  });    //注:require.resolve()将返回node.js需要的模块路径。</code></pre>    <p>接下来4.2中会解释 peerDependencies 和 dependencies 的区别。</p>    <p>4.1.2 执行顺序实例</p>    <pre>  <code class="language-javascript">// In my-generator/generators/turbo/index.js  module.exports = require('yeoman-generator').Base.extend({    'prompting' : function () {      console.log('prompting - turbo');    },      'writing' : function () {      console.log('writing - turbo');    }  });    // In my-generator/generators/electric/index.js  module.exports = require('yeoman-generator').Base.extend({    'prompting' : function () {      console.log('prompting - zap');    },      'writing' : function () {      console.log('writing - zap');    }  });    // In my-generator/generators/app/index.js  module.exports = require('yeoman-generator').Base.extend({    'initializing' : function () {      this.composeWith('my-generator:turbo');      this.composeWith('my-generator:electric');    }  });</code></pre>    <p>来分析下上面这段脚本:</p>    <ol>     <li> <p>以上这段脚本在初始化的时候执行了两个 composeWith 方法 turbo 和 electric。</p> </li>     <li> <p>分别执行了他们目录下的index.js。</p> </li>     <li> <p>加载顺序判断:turbo 优先于 electric。</p> </li>     <li> <p>生命周期问题:prompting 优先于 writing。</p> </li>    </ol>    <p>所以执行后的结果如下:</p>    <pre>  <code class="language-javascript">prompting - turbo  prompting - zap  writing - turbo  writing - zap</code></pre>    <h3>4.2 peerDependencies 和 dependencies 的区别</h3>    <p>npm允许以下三种dependencies(依赖):</p>    <ol>     <li> <p>dependencies :使用依赖,自己或是别人使用你的generator所必备的依赖模块。这些模块被generator视为本地模块。</p> </li>     <li> <p>peerDependencies :看下面的 <strong>注:</strong> npm@3后,peerDependencies不会再被自动安装,需要手动。</p> </li>     <li> <p>devDependencies :开发依赖,作为开发或者是测试需要用的模块,如果别人安装你的generator,这些模块不应该被安装。</p> </li>    </ol>    <p>当使用 peerDependencies 别的模块也要依赖当前这个模块,小心不要创建版本导致冲突,Yeoman推荐使用(>=) 或 (*) 来安装可用的版本,如:</p>    <pre>  <code class="language-javascript">{    "peerDependencies": {      "generator-gruntfile": "*",      "generator-bootstrap": ">=1.0.0"    }  }</code></pre>    <p>注:npm@3以后, peerDependencies 不会再被自动安装,安装他们必须执行如下:</p>    <pre>  <code class="language-javascript">npm install generator-yourgenerator generator-gruntfile generator-bootstrap@">=1.0.0"</code></pre>    <h2>(五)依赖管理</h2>    <p><a href="/misc/goto?guid=4959670077969823439" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <p>Yeoman提供以下几种形式来安装依赖。</p>    <h3>5.1 npm</h3>    <p>使用 generator.npmInstall() 来安装npm包,如果你在多个generators调用了 npm install Yeoman保证只会执行一次。</p>    <p>例如:你需要安装 lodash 这个模块作为发开依赖。</p>    <pre>  <code class="language-javascript">generators.Base.extend({    installingLodash: function() {      this.npmInstall(['lodash'], { 'saveDev': true });    }  });</code></pre>    <p>效果等同于直接在终端输入:</p>    <pre>  <code class="language-javascript">npm install lodash --save-dev</code></pre>    <h3>5.2 Bower</h3>    <p>使用 generator.bowerInstall() 来安装依赖。实例:同npm。</p>    <h3>5.3 Both npm & Bower</h3>    <p>使用 generator.installDependencies() 来同时安装npm 和 bower。实例:同npm。</p>    <h3>5.4 Using other tools</h3>    <p>可以使用 spawnCommand 来安装其他工具。比如:PHP的composer。</p>    <h2>(六)文件操作</h2>    <p><a href="/misc/goto?guid=4959670078055188467" rel="nofollow,noindex">原文地址:http://yeoman.io/authorin...</a></p>    <h3>6.1 根路径</h3>    <p>Yeoman会在这个根路径中创建你项目的脚手架。</p>    <p>根路径会以以下两种方式定义:</p>    <ol>     <li> <p>当前工作路径</p> </li>     <li> <p>最近一级中包含 .yo-rc.json 的路径</p> </li>    </ol>    <p>你可以通过Yeoman提供的 generator.destinationRoot() 方法来获取根路径,这个方法接收一个参数 generator.destinationPath('sub/path') 来获取子目录的路径。</p>    <p>例如:</p>    <p>查看当前路径</p>    <pre>  <code class="language-javascript">$ pwd  ~/projects</code></pre>    <pre>  <code class="language-javascript">//跟路径是 ~/projects  generators.Base.extend({    paths: function () {      this.destinationRoot();      // returns '~/projects'        this.destinationPath('/sub/index.js');      // returns '~/projects/sub/index.js'    }  });</code></pre>    <h3>6.2 常用工作路径</h3>    <p>原文是Template context,其实我感觉直译不太好,换做叫常用工作路径会更好。</p>    <p>这个路径的默认取你当前目录 ./templates/ , 可以手动覆盖这个路径 generator.sourceRoot('new/template/path')</p>    <p>例如:</p>    <pre>  <code class="language-javascript">generators.Base.extend({    paths: function () {      this.sourceRoot(); //设置常用工作路径      // returns './templates'        this.templatePath('index.js'); //读取常用工作路径      // returns './templates/index.js'    }  });</code></pre>    <h3>6.3 文件操作</h3>    <p>所有文件相关的方法都会通过 this.fs 暴露出来。 <a href="/misc/goto?guid=4959664199230894360" rel="nofollow,noindex">这里有所有文件操作相关方法</a> ,包括下面的 copyTpl 方法。</p>    <p>实例: 把一个 常用工作路径 的文件复制到 根路径 下,并传一个参数。</p>    <p>1.常用工作路径下的 ./templates/index.html 内容是:</p>    <pre>  <code class="language-javascript"><html>    <head>      <title><%= title %></title>    </head>  </html></code></pre>    <p>2.我们用 copyTpl 方法把来复制文件,该方法使用 <a href="/misc/goto?guid=4959670078175502650" rel="nofollow,noindex">ejs模板语法</a></p>    <pre>  <code class="language-javascript">generators.Base.extend({    writing: function () {      this.fs.copyTpl(        this.templatePath('index.html'),//第一个参数:from        this.destinationPath('public/index.html'),//第二个参数:to        { title: 'Templating with Yeoman' }//第三个参数:options      );    }  });</code></pre>    <p>3.来看复制后的 public/index.html</p>    <pre>  <code class="language-javascript"><html>    <head>      <title>Templating with Yeoman</title>    </head>  </html></code></pre>    <h3>6.4 通过「流」来改变文件</h3>    <p>Yeoman提供 registerTransformStream() 方法,使用gulp的来操作文件。</p>    <p>例如:</p>    <pre>  <code class="language-javascript">var beautify = require('gulp-beautify');  this.registerTransformStream(beautify({indentSize: 2 }));</code></pre>    <h3>6.5 修改已经存在文件的内容</h3>    <p>Yeoman介绍了几个比较流行的解析器:</p>    <p>1. <a href="/misc/goto?guid=4959615071852619541" rel="nofollow,noindex">Cheerio</a> for parsing HTML,基本实现流程如下</p>    <pre>  <code class="language-javascript">var cheerio = require('cheerio'),      $ = cheerio.load('<h2 class="title">Hello world</h2>');    $('h2.title').text('Hello there!');  $('h2').addClass('welcome');    $.html();  //=> <h2 class="title welcome">Hello there!</h2></code></pre>    <p>2. <a href="/misc/goto?guid=4959615953968845888" rel="nofollow,noindex">Esprima</a> for parsing JavaScript</p>    <p>3. For JSON files <a href="/misc/goto?guid=4958876160020827053" rel="nofollow,noindex">可以使用JSON原生的方法</a></p>    <p>本次只翻译了前六章,后续会翻译后六章、自己如果写一个generator以及遇到的坑和问题。都会更新在我的github的Yeoman-article中。</p>    <p>github: <a href="/misc/goto?guid=4959670078342913106" rel="nofollow,noindex">https://github.com/tonyljl526/yeoman</a></p>    <p>来自: <a href="/misc/goto?guid=4959670078434797712" rel="nofollow">https://segmentfault.com/a/1190000004896264</a></p>