项目工程化-通用代码生成

759 阅读5分钟

起因

最近刚刚完成了一个前端To C的项目,想着把搭建的框架抽成一个脚手架的template,说起脚手架,不得不提的就是业界的老大哥yeoman,还有就是最近几年比较火的create-react-appvue-cliangular-cli,相对与专业度而言,后面三款是针对于单一的框架来写的,集成度比较高,社区经验比较丰富。

技术栈

先说一下我们的技术栈,我们使用的nextjs + koa

nextjs处理React组件的SSR渲染,而koakoa-middlewares则负责用户状态管理和接口代理。

因为我们用的是react的技术栈,针对业务上比较多的通用功能,我们也希望能够通过组件模板的方式,对组件进行动态的生成。

举个栗子,在我们开发的过程中,经常会有某些页面要判断用户是否已经登录,或者参数不正确的情况,此时我们的通用处理是跳转到主页。

因为是SSR的项目,在服务端的时候我们已经可以知道用户是否登录了,此时我们可以直接调用nextjs的钩子函数getServerSideProps,一段伪代码如下:

export async function getServerSideProps({req:http.IncomingMessage,res:http.ServerResponse} ) {
  if (!userLogin) {
      res.writeHead(302, {
      	location: '/home'
      });
      res.end();
  }
  return {
    props: {}, 
  }
}

如上代码我们都可以通过代码生成工具来一键生成,目前我能想到有如下方案。

  • 自定义js模板字符串,通过js对象动态传入值,搭配nodefs接口渲染文件
  • snippets

常规的Simple React Snippets 可以通过简单的字母组合来生成一段react代码。

我对项目的结构进行了优化,将原写在pages目录下的代码拆分为两部分,分别放到pagesviews目录下。

  • pages目录下的组件进行路由的控制,以及服务端数据的抓取
  • views目录下存放着组件和以及样式文件

这样做的好处是,如果后续SSR想换成CSR,重写pages目录下代码即可。

简单来看的话我们的目录是下面这个样子的

├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── src
│   ├── pages
│   │   ├── _app.js
│   │   ├── home.js
│   │   ├── index.js
│   └── views
└── styles
    ├── Home.module.css
    └── globals.css

如果我们想创建一个订单查询页面,那我要同时去创建如下两个个文件src/pages/order/index.js,src/views/order/index.js

站在工程化的角度,我们可以使用工具一键创建这些代码,snippets的问题是需要先去创建一个文件,然后在文件中去生成,此处我们要创建的文件比较多,可以使用一个小工具plop, 简单来讲,plop的工作原来是通过终端交互的方式获取参数,根据事先定义好的模板和获取到的参数进行结合,生成代码到指定的目录下。

下面我将一步步来搭建一个使用plopnextjs项目。 使用如下命令来初始化一个nextjs项目

先简单调整一下代码结构,使用如下命令

mkdir src && mv pages src
mkdir -p src/views

调整_app.jsindex.js中styles文件夹的相对路径。

index.js

- import styles from '../styles/Home.module.css'
+ import styles from '../../styles/Home.module.css'

_app.js

- import '../styles/globals.css'
+ import '../../styles/globals.css'

新建一个测试页面

mkdir -p src/pages/my && touch src/pages/my/setting.js
mkdir -p src/views/my && touch src/views/my/setting.js

src/pages/my/setting.js代码如下

import SettingView from '../../views/my/setting'

const SettingPage = () => {
    return <>
        <SettingView />
    </>
}

export default SettingPage
export async function getServerSideProps({ req, res }) {
    const login = true;
    if (!login) {
        res.writeHead(302, { location: '/' })
        res.end();
    }
    return {
        props: {}, // will be passed to the page component as props
    }
}

src/views/my/setting.js 代码如下

const SettingView = () => {
    return <div>SettingView</div>
}

export default SettingView

站在业务的角度,如果我想对自己的账号进行设置,则需要先进行登录,此处直接通过更改变量来测试,不引入接口。

启动项目

npm run dev

浏览器打开链接http://localhost:3000/my/setting,此处我们模拟的是用户已经登录情况,可以看到浏览器已经显示了

更改src/views/my/setting.js中代码

- const login = true;
+ const login = false;

刷新页面,可以看到浏览器跳转到了http://localhost:3000,看一下网络

关于此处的逻辑,简单来想是可以抽象成一套模板的,假定后面我有一些其他的逻辑,比如订单查看,密码修改,逻辑是类似的。

我们引入plop小工具

npm i plop -D
touch plopfile.js 
mkdir -p plop-templates/login-controller
touch plop-templates/login-controller/view.hbs
touch plop-templates/login-controller/page.hbs

修改package.json

{
	"script": {
    	// other scripts
    	"plop": "plop"
    }
}

如下是抽取出来的模板代码 view.hbs

const {{firstUpperCase name}}View = () => {
    return <div>{{firstUpperCase name}}View</div>
}

export default {{firstUpperCase name}}View

page.hbs

import {{firstUpperCase name}}View from '../../views/{{model}}/{{name}}'

const {{firstUpperCase name}}Page = () => {
    return <>
        <{{firstUpperCase name}}View />
    </>
}

export default {{firstUpperCase name}}Page
export async function getServerSideProps({ req, res }) {
    const login = false;
    if (!login) {
        res.writeHead(302, { location: '/' })
        res.end();
    }
    return {
        props: {}, // will be passed to the page component as props
    }
}

如下为plopfile.js的代码

const modelNames = ['my', 'order'];

module.exports = async function (
    /** @type {import('plop').NodePlopAPI} */
    plop
) {
    plop.setHelper('firstUpperCase', function (text) {
        return text && text.replace(/^\S/, s => s.toUpperCase());
    });

    plop.setGenerator('login-controller', {
        description: '新增带有登录逻辑判断的代码',
        prompts: [
            {
                type: 'list',
                name: 'model',
                message: '请选择模块名',
                choices: modelNames.map((item, index) => ({
                    key: index.toString(),
                    name: item,
                    value: item
                })),
            }, {
                type: 'input',
                name: 'name',
                message: '请输入组件名称'
            }
        ],
        actions: [
            {
                type: 'add',
                path: 'src/pages/{{model}}/{{name}}.js',
                templateFile: 'plop-templates/login-controller/page.hbs'
            },
            {
                type: 'add',
                path: 'src/views/{{model}}/{{name}}.js',
                templateFile: 'plop-templates/login-controller/view.hbs'
            }
        ]
    });
};

简单介绍一下这段代码

  • /** @type {import('plop').NodePlopAPI} */是为了引入typescript的声明文件,方便我们编写配置
  • plop.setHelperhandlebars的转换工具函数,作用是将首字母大写
  • setGenerator 设置生成器,如果有多个的话,在执行的时候会提示去选择
  • prompts 是使用问答的方式让开发同学来填写信息
  • type:list是通过单选的方式来让开发同学选择
  • type:input是通过输入的方式来让开发同学选择
  • actions是与终端交互后执行的动作,我们目前有两个动作,是同时根据模板在指定目录下新增了两个文件

看一下终端的执行效果

此处只是起到抛砖引玉的效果,有兴趣的小伙伴可以去官网看一下详细的教程。

demo所在路径为https://github.com/huomarvin/next-plop-demo,感兴趣的同学,可以去查看。