#### 安装教程

npm install -g alipic-tool


##### 前面的话

在前端工程化过程中，为了解决多项目中，相似度高的工作，便诞生许多前端脚手架，这里记录下自己实现一个简易前端脚手架过程的实践。主要是解决多个页面相似内容的复制粘贴问题，功能类似于Webstorm的Live template，或者Vscode的Snippets。

##### 思路

- 预先配置页面模板，预留关键字变量

- 用户填写关键字变量，生成页面模板，输出到制定目录

##### 用到的包

- fs

    读写文件模块，这里主要用于读入用户配置文件，输出模板到文件


- commander

    NodeJs命令行工具，提供了用户命令行输入和参数解析，用户解析用户输入


- inquirer

    NodeJs交互式命令行工具，询问操作者问题，获取用户输入，校验回答的合法性


- metalsmith

    文件处理，读写操作


- handlebars

    将模板中的变量替换为用户输入，编译模板，类似框架如：artTemplate，Jade


- path

    NodeJs的路径操作库，如合并路径


- chalk

    命令行输出样式美化


##### 具体实现

1. 首先在一个新的文件夹，如xxx-tools下 `npm init` 创建一个node项目，因为是要做成一个npm包的脚手架，所以在包的取名上一定要唯一，即`package.json`中`name`字段，避免在发包的时候和网上已经存在的npm包重名，报403没有权限的错。

2. 在xxx-tools文件夹下创建`bin`文件夹，同时在`bin`文件夹下创建脚本`tempTool`文件，内容如下：

```js
#!/usr/bin/env node

console.log('Hello World');

```

注意哦，`#!/usr/bin/env node` 这个是Linux规范，用来指明了这个执行脚本的解释程序，要是没有这一行，默认用当前Shell去解释这个脚本

3. 在`package.json`中增加`bin`配置：

```json
 "bin": {
    "tempTool": "./bin/tempTool"
  },

```

4. 到目前为止，一个简单的前端脚手架实现了，在npm官网注册，在项目里执行`npm login`登录，之后`npm publish`如果一切顺利，npm包提交完毕，可以在其它项目中执行`npm i -g xxx-tools`，安装这个包，执行`xxx-tools`命令，输出 `Hello World`，脚手架开发过程中，也涉及到在本地调试，可以直执行`node ./bin/xxx-tools`

5. 现在来加入具体的开发流程，用户的输入，输入信息的读取等等，`bin`文件修改如下

```js
#!/usr/bin/env node

const program = require('commander');
const chalk = require('chalk');
const { loadTemplate } = require('../src/lib/writeTemp');

const log = data => console.log(chalk.green(data));

log('初始化模板配置');

program
  .command('create')
  .description('create template')
  .option('-d')
  .action(async function () {
    const result = await loadTemplate();
    result ? null : log('配置完毕');
  });

program.parse(process.argv);
```

用户执行`create`命令，在这里调用了`loadTemplate`函数，看一下这个函数

```js
// 把模板中的变量替换为用户输入的变量，输出模板到制定文件夹
const Metalsmith = require('metalsmith');
const Handlebars = require('handlebars');
const path = require('path');
const fs = require('fs');
const { askQuestion } = require('./askQuestion');

const loadTemplate = async () => {
  // 从toolrc.json文件读取配置
  const dirPath = process.cwd();
  if (!fs.existsSync('toolrc.json')) {
    throw new Error('toolrc.json配置文件不存在');
  }
  const configJson = path.join(dirPath, 'toolrc.json');
  const config = fs.readFileSync(configJson);
  const { source, dist, questionConfig } = JSON.parse(config);
  const answer = await askQuestion(questionConfig);
  const metalsmith = Metalsmith(__dirname);

  metalsmith
    .metadata(answer)
    .source(path.join(dirPath, source))
    .destination(path.join(dirPath, dist))
    .use(function (files, metalsmith, done) {
      //遍历替换模板
      Object.keys(files).forEach(fileName => {
        const fileContentsString = files[fileName].contents.toString();
        //Handlebar compile 前需要转换为字符串
        files[fileName].contents = new Buffer(Handlebars.compile(fileContentsString)(metalsmith.metadata()));
      });
      done();
    }).build(function (err) {
    if (err) throw err;
  });
};

module.exports.loadTemplate = loadTemplate;

```

为了方便用户配置，需要用户自行配置一个`toolrc.json`文件，指明模板文件的输入输出目录，和需要用到的
询问变量，示例配置如下：

``` json
{
  "source": "/src/template",
  "dist": "/build",
  "questionConfig": {
    "name": {
      "type": "string",
      "required": true,
      "label": "Module name"
    },
    "description": {
      "type": "string",
      "required": true,
      "label": "Module description"
    },
    "namespace": {
      "type": "string",
      "required": true,
      "label": "dva model namespace"
    }
  }
}
```

`source`配置了模板文件的位置，`dist`为输出文件的位置，`questionConfig`为模板中的关键字，需要用户在交互的命令行中输入，下面这段为利用`inquirer`包，实现命令行交互。


```js
// 遍历问题模板，输出提问

const inquirer = require('inquirer');

const askQuestion =  async (prompts)=> {
  let promptsArr = Object.keys(prompts).map(key => ({
    name: key,
    ...prompts[key]
  }));
  return inquirer.prompt(promptsArr);
}

module.exports.askQuestion = askQuestion
```

效果如下：
![image](src/images/tools.jpg)

因为用了`handlebars`包，模板的定义需要符合其规范，模板文件如下：

```js
import React, { Component } from 'react';
import { connect } from 'dva';
import './style.less';

@connect(state => ({ loading: state.loading }))
class {{name}} extends React.Component {
  state = {};

  componentWillReceiveProps = (nextProps) => {

  };

  render() {
    return ();
  }

}

export default {{name}};

```

最终输出到 `dist` 目录的文件，会替换其中双括号里的内容

##### 结束的话

这里只是简单的例子，可以沉淀一些业务场景的模板，通过命令行的方式快速的创建，避免复制粘贴，其实本意是学习一下Node的脚手架工具的实现，有兴趣的同学可以看看`babel-cli`的源码。