使用Node工具簡化文件建立流程

背景

最近搭建畢設前端框架的時候,每當建立一個頁面,我都須要建立這個頁面組件,建立它的Route,最後將該Route加入到總的Route。固然,這裏的流程還不算複雜,基本也是複製粘貼改改變量,可是後面還要用到Redux,可能還會使用saga…再將它們加入這一條流程線,我須要改的東西又多了。html

在公司實習的腳手架裏,發現有大佬造的輪子,以前也只是照着命令敲拿來用,此次順帶研究了一下核心功能,結合個人畢設框架須要,加入了最簡單的自動化「腳本」。前端

所需環境和工具

  1. Node環境下執行。node

  2. 命令映射,使用commanderreact

    讓文件能夠經過命令行的形式執行git

  3. 文件讀寫,這裏我使用的是fs-extra,使用Node自帶的File System,可是前者支持Promise和Async, Await。github

    文件讀寫只是讀取模板文件內容,而後寫入到新的文件爲咱們所用shell

  4. 模板字符串,使用lodash/string的模板字符串方法templatejson

    模板字符串:咱們可使用xxx.tmpl格式的文件存儲咱們的模板,須要替換的內容使用ubuntu

    <%= xxx %>表示便可,下面會給出文件原型api

  5. 文件修改,使用ts-simple-ast

    文件修改則是直接修改原來文件,加入本身所需的東西,例如修改變量值,這也是這篇文章中提到的較爲簡單的一個用途,其餘更復雜的也能夠參考文檔學習

文件原型和需求

0. 需求

每當新增一個頁面,咱們須要建立一個基本框架組件,一個Route,最後把這個Route自動插入到總的Router裏。

1. xxxComponent

這裏建立了一個很是簡單的組件,帶有Props和State,interface使用Ixxx命名。

import React from 'react';

interface I<%= featureUpperName %>Props {}

interface I<%= featureUpperName %>State {}

export default class <%= featureUpperName %> extends React.Component<I<%= featureUpperName %>Props, I<%= featureUpperName %>State> {
    constructor(props: I<%= featureUpperName %>Props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <h2>My Home</h2>
        )
    }
}
複製代碼

2. xxx.index

這個文件里加入全部須要導出的Component,並做爲統一導出出口。

export { default as <%= featureUpperName %> } from './<%= featureUpperName %>'
複製代碼

3. Route

自定義的Route,屬性也基本遵循原生Route,加入loadable component,支持按需加載。

import App from '../common/component/App';
import { IRoute } from '@src/common/routeConfig';

const loader = (name: string) => async () => {
    const entrance = await import('./');
    return entrance[name];
};

const childRoutes: IRoute[] = [{
    path: '/<%= featureName %>',
    name: '<%= featureUpperName %>',
    loader: loader('<%= featureUpperName %>'),
}]

export default {
    path: '/<%= featureName %>',
    name: '',
    component: App,
    childRoutes
};
複製代碼

上面三個便做爲基本模板文件,下面這個則是總的Route

4. routeConfig

完成一個頁面的建立並生成它的route後,須要在該文件引入這個route,而後修改變量childRoutes,插入該route,這樣咱們的工做就算完成啦。

import HomeRoute from "../features/home/route";

export interface IRoute {
    path: string
    name: string
    component?: React.ComponentClass | React.SFC;
    childRoutes?: IRoute[]
    loader?: AsyncComponentLoader
    exact?: boolean
    redirect?: string
}

const childRoutes: IRoute[] = [HomeRoute]

const routes = [{
    path: '/',
    name: 'app',
    exact: true,
    redirect: '/home'
}, ...childRoutes]

export default routes;
複製代碼

步驟

零、源代碼

1. kit.js

用於讀取模板文件,寫入新的文件

首先第一行,告訴shell此文件默認執行環境爲Node。

接下來咱們來看addFeatureItem(忽略個人命名╮(╯▽╰)╭),這個函數有三個參數:

  • srcPath,template文件位置
  • targetPath,寫入的文件位置
  • option,渲染模板時使用,簡而言之能夠替換掉模板中的變量爲裏面咱們設定的值

咱們先確認文件是否存在,而後讀取模板文件,寫入新的文件便可,中間加了個已有文件判斷。

是否是很簡單!

最後加入使用commander建立本身的命令便可,更詳細的用法能夠查看commander的文檔,這裏添加一個簡單的add命令,後跟一個featureName,鍵入命令後執行action函數,裏面的參數即咱們剛剛鍵入的featureName,讀取後即可以從模板建立新的feature。

固然,咱們還須要修改routeConfig.ts這個文件,我將這個操做放到了下面的ts-ast.ts文件。

#! /usr/bin/env node

const fse = require('fs-extra');
const path = require('path');
const _ = require('lodash/string')
const commander = require('commander')
const ast = require('./ts-ast')

const templatesDir = path.join(__dirname, 'templates');
const targetDir = path.join(__dirname, '..', 'src', 'features');

async function addFeatureItem(srcPath, targetPath, option) {
    let res;
    try {
        await fse.ensureFile(srcPath)
        res = await fse.readFile(srcPath, 'utf-8')
        
        // avoid override
        const exists = await fse.pathExists(targetPath)
        if(exists) {
            console.log(`${targetPath} is already added!`);
            return
        }

        await fse.outputFile(targetPath, _.template(res)(option), {
            encoding: "utf-8"
        })
        console.log(`Add ${srcPath} success!`);
    } catch (err) {
        console.error(err)
    }
}

async function addFeature(name) {
    const renderOpt = {
        featureName: name,
        featureUpperName: _.upperFirst(name)
    }

    const componentTmpl = `${templatesDir}/Component.tsx.tmpl`;
    const component = `${targetDir}/${name}/${_.upperFirst(name)}.tsx`;
    addFeatureItem(componentTmpl, component, renderOpt);

    const indexTmpl = `${templatesDir}/index.ts.tmpl`;
    const index = `${targetDir}/${name}/index.ts`;
    addFeatureItem(indexTmpl, index, renderOpt);

    const routeTmpl = `${templatesDir}/route.ts.tmpl`;
    const route = `${targetDir}/${name}/route.ts`;
    addFeatureItem(routeTmpl, route, renderOpt);
}

commander
    .version(require('../package.json').version)
    .command('add <feature>')
    .action((featureName) => {
        // add features
        addFeature(featureName);
        // manipulate some ts file like route
        ast(featureName);
    })

commander.parse(process.argv);
複製代碼

2. ts-ast.js

用於修改rootConfig.ts文件

先給出ts-simple-ast的地址,本身仍是以爲這個操做是比較複雜的,我也是參考了文檔再加上項目腳手架代碼纔看明白,至於原理性的東西,可能還須要查看Typescript Compiler API,由於這個包也只是Wrapper,文檔也還不是很完善,更復雜的需求還有待學習。

這裏關鍵就兩個操做,一個是添加一個import,其次則是修改childRoutes變量的值。可是一些函數的英文字面意思理解起來可能比上面的文件讀寫要困難。

  1. 咱們首先須要新建一個Project
  2. 而後須要獲取待操做的文件,拿到待操做文件(srcFile)以後,直接使用addImportDeclaration這個方法即可以添加default import,若是須要named import,也可使用addNamedImport。定義好default name(defaultImport prop)以及path(moduleSpecifier)便可。
  3. 最後是對childRoutes變量的值進行修改,這一過程比較複雜:
    • 首先須要經過變量名拿到一個VariableStatement
    • 而後再拿到變量聲明(VariableDeclaration),由於一個文件中可能有多處聲明(函數中,全局中)
    • 因爲該例中只有一處聲明,因此咱們直接forEach遍歷聲明便可,遍歷時拿到initializer(這裏是一個相似array的東東)
    • 再使用它的forEachChild遍歷拿到node,node.getText終於拿到了裏面的一個值(好比HomeRoute)
    • 咱們將這些值添加到一個新的數組
    • 直到遍歷完畢,再將它們拼接起來,加入新的route,以字符串形式setInitializer便可
  4. 最後保存全部操做,Project.save()便可
const { Project } = require('ts-simple-ast')
const _ = require('lodash/string')
const path = require('path')

const project = new Project();
const srcDir = path.join(__dirname, '..', 'src', 'common');

async function addRoute(featureName) {
  const FeatureName = _.upperFirst(featureName);
  try {
    const srcFile = project.addExistingSourceFile(`${srcDir}/routeConfig.ts`);
    srcFile.addImportDeclaration({
      defaultImport: `${FeatureName}Route`,
      moduleSpecifier: `../features/${featureName}/route`
    })
    const routeVar = srcFile.getVariableStatementOrThrow('childRoutes');

    let newRoutes = [];

    routeVar.getDeclarations().forEach((decl, i) => {
      decl.getInitializer().forEachChild(node => {
        newRoutes.push(node.getText());
      })
      decl.setInitializer(`[${newRoutes.join(', ')}, ${FeatureName}Route]`);
    })

    await project.save();
    console.log("Add route successful");
  } catch(err) {
    console.log(err);
  }
  
}

module.exports = addRoute;
複製代碼

1、修改文件權限,使其可執行

只要頭部加入了#! /usr/bin/env node,簡單的一行命令便可搞定chmod +x /filePath/yourExeFile

而後,咱們即可以使用/filePath/yourExeFile add featureName的方式添加一個新的頁面!

參考

  1. what-does-chmod-x-filename-do-and-how-do-i-use-it
相關文章
相關標籤/搜索