一键初始化新页面 npm script -- 实践技能总结

1,301 阅读2分钟

背景

业务的 h5 工程是基于 Koa 封装的 node 应用,前端采用的是多页面的方式,目录大概如下:

├── bin
├── app
│   ├── controllers
│   ├── routes
│   └── views
├── src
│   ├── pages
│   ├── common

在新增一个页面的时候,需要这样做:

  • app/routes 中增加路由
  • app/controllers 中增加路由对应的中间件函数
  • app/views 中增加视图模版 html
  • src/pages 中新建页面文件夹,以及里边的 .vue .js .less 等文件

对一个懒人来说,这种复制粘贴的过程简直不能忍!而且极容易漏掉某个地方,服务启不起来还要查 pm2 日志来找错误,仅仅初始化一个新页面就要几分钟。。。

于是就写了一个 npm script 来一键搞定,在此记录下流程和技能点。

功能预览:

1. 定义命令

既然是初始化页面,就叫他 initPage,传参遵循了工程的 devbuild 命令的传参方式(虽然有点怪):npm run initPage --{path},例如:

npm run initPage --pages/test

2. 校验参数

在 bin 目录下新建 init-page/index.js,首先进行参数的获取和校验

// 校验参数(文件名)
const argv = JSON.parse(process.env.npm_config_argv).original;
let fileName;
argv.forEach((item) => {
    if (item.indexOf('--') === 0) {
        fileName = item.replace('--', '');
    }
});
if (/[^\w-/]/g.test(fileName)) {
    error(`请输入正确的文件路径!\n${fileName}`);
    process.exit(0);
}

3. 确定目录

参数校验完毕,需要确定新页面加在哪里,也就是目标路径。这里约定输入参数路径的基路径为 src/pages,也就是说如果输入的参数为 test ,目标路径就是 src/pages/test,但是通常我们组的页面大多写在 src/pages/main,所以做了一个提示。 这里需要用到 inquirer 定义命令行交互。

if (!bizDir.includes(fileName.split('/')[0])) {
    warn(`src/pages 下没有该目录:${fileName.split('/')[0]}\n`);
    inquirer
        .prompt([
            {
                name: 'useDefault',
                message: '是否要加在 src/pages/main 目录下?(Y/n)'
            },
        ])
        .then((answer) => {
            let targetDirPath;
            if (answer.useDefault === '' || answer.useDefault === 'Y' || answer.useDefault === 'y') {
                targetDirPath = path.resolve(bizDirPath, 'main', fileName);
            } else {
                targetDirPath = path.resolve(bizDirPath, fileName);
            }
            initPage(targetDirPath, fileName);
        });
} else {
    const targetDirPath = path.resolve(bizDirPath, fileName);
    initPage(targetDirPath, fileName);
}

4. 添加 controller、router

这里需要在相应文件里新增函数或属性,所以自然需要用到 AST,相关的工具:babylonbabel-traversebabel-typesbabel-templatebabel-generator。 以添加 controller 为例

const addController = (fileName) => new Promise((resolve, reject) => {
    const nameArr = fileName.split('/');
    const name = nameArr[0];
    const controllerName = toCamel(fileName);
    const controller = path.resolve(projectRootPath, 'app/controllers', `${name}.js`);
    isExist(controller).then((exist) => {
        controllerExist = exist;
        if (exist) {
            fs.readFile(controller, 'utf8', (err, data) => {
                if (err) throw err;
                const ast = parse(data);
                traverse(ast, {
                    ExpressionStatement(path) {
                        const operatePath = path;
                        path.traverse({
                            MemberExpression(path) {
                                if (path.get('object').isIdentifier({name: 'module'})) {
                                    // 添加控制器
                                    const ast = controllerTmpl({
                                        CONTROLLER_NAME: t.identifier(controllerName),
                                        PATH: t.stringLiteral(fileName),
                                    });
                                    operatePath.insertBefore(ast);
                                    // 添加导出的属性
                                    const rightPath = path.parentPath.get('right');
                                    if (rightPath) {
                                        const propertiesPath = rightPath.get('properties');
                                        const lastChild = propertiesPath[propertiesPath.length - 1];
                                        lastChild.insertAfter(t.objectProperty(t.identifier(controllerName), t.identifier(controllerName), false, true));
                                    }
                                }
                                path.skip();
                            },
                        });
                        path.skip();
                    },
                });
                const output = generate(ast, {}, data);
                fs.writeFile(controller, output.code, (err) => {
                    if (err) throw err;
                    success(`${name}.js 已配置`);
                    resolve();
                });
            });
        } else {
            fs.writeFile(controller, newControllerFile(fileName), (err) => {
                if (err) throw err;
                success(`${name}.js 已配置`);
                resolve();
            });
        }
    });
});

5. Eslint fix

通过语法转换生成的新文件存在不符合 eslint 规范的问题,这里用到 node child_process 模块,以及好看的命令行 loading ora

const {exec} = require('child_process');
const ora = require('ora');

const loading = ora('eslint fixing ...');
const projectRootPath = path.resolve(__dirname, '../../');

const eslintFix = () => new Promise((resolve, reject) => {
    loading.start();
    exec('./node_modules/.bin/eslint --ext .js --fix app/controllers/** app/route/**', {
        cwd: projectRootPath,
    }, (err, stdout, stderr) => {
        if (err) {
            loading.fail();
            error(err);
            resolve();
            return;
        }
        loading.succeed();
        success('eslint fix 已完成');
        resolve();
    });
});

6. 启动服务监听页面

判断 docker 容器是否运行并进入容器启动服务

const ifStartNow = (runing, fileName) => {
    inquirer
        .prompt([
            {
                name: 'startNow',
                message: '是否立即启动项目?(Y/n)',
            },
        ])
        .then((answer) => {
            if (answer.startNow === '' || answer.startNow === 'Y' || answer.startNow === 'y') {
                if (runing) {
                    console.log('\n正在进入容器...');
                    execSync(`docker exec -it xxx-container bash -c "npm run dev --${fileName}"`, {
                        cwd: projectRootPath,
                        stdio: 'inherit',
                    });
                } else {
                    console.log('\n正在启动容器:npm start ...');
                    console.log(`稍后请输入监听页面${fileName}`);
                    execSync('npm start', {
                        cwd: projectRootPath,
                        stdio: 'inherit',
                    });
                }
            } else {
                console.log(`\n稍后启动容器执行 ${chalk.bold.blue(`npm run dev --${fileName}`)} 查看页面~`);
            }
        });
};

Github: github.com/onlyil/init…