脚手架项目开发小手册

carbon.png

基础概念

随着前端工程化的概念越来越深入人心,脚手架应运而生。简单来说,「前端脚手架」就是指通过选择几个选项快速搭建项目基础代码的工具。前端脚手架可以有效避免我们 ctrl + C 和 ctrl + V 相同的代码框架和基础配置。

脚手架思路

在动手开始开发脚手架 CLI 之前我们先捋一下思路,纵览业界比较流行的几个脚手架,会发现虽然它们功能丰富度和复杂程度不一样,但是总体来说都会包含以下基本功能

CLI 搭建项目

  • 根据用户输入生成配置文件
  • 下载指定项目模板
  • 在目标目录生成新项目

CLI 运行项目

  • 本地启动预览
  • 热更新
  • 语法、代码规范检测

脚手架架构图

通过架构图了解下脚手架的大致工作流程


插件依赖

开发脚手架常用插件

插件名称 简述
commander node.js命令行界面的完整解决方案
download-git-repo 远程下载git项目
figlet 艺术字
handlebars 模板引擎
inquirer 常见的交互式命令行用户界面的集合
log-symbols 为各种日志级别提供着色的符号
child_process 执行命令包
ora 进度条 等待状态
chalk 字体加色

创建脚手架

初始化项目

下载node,创建一个新的文件夹,cmd进入到文件夹目录运行:npm init,生成 package.json 文件

{
    
    
  "name": "@gfe/cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "xcc",
  "license": "ISC"
}

进入项目

修改 package.json 中的 bin 参数,专门放置用户的自定义命令,指定可执行文件的位置,bin 里的命令是可执行命令,模块安装的时候如果是全局安装,则 npm 会为 bin 中配置的文件创建一个全局软连接,在命令行工具里可以直接执行

{
    
    
  "name": "@gfe/cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin":{
    
    
  	"gfe-cli": "src/gfe-cli.js"
  },
  "scripts": {
    
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "xcc",
  "license": "ISC"
}

项目中新建 src 文件夹,新增 gfe-cli.js,文件为命令入口文件,文件第一行必须是 #!/usr/bin/env node,代表执行这个脚本的时候,调用/usr/bin下的node解释器。

处理命令行

我们通过 commander 来设置不同的命令。
command 方法设置命令的名字、description 方法是设置命令的描述、alias 方法设置命令简称【懒人必备】、options 设置命令需要的参数。commander 更详细的文档可以去 commander官网查看。
我们脚手架先加入三个命令:项目创建、项目初始化、项目启动。源代码在 src/gfe-cli.js 中。

项目模板

脚手架可以帮助我们快速生成一套指定的项目结构和配置,最常用的方式就是我们提前准备好一套通用的、易用的、规范的项目模板存放在指定位置,在脚手架执行创建项目命令的时候,直接将 **准备好的模板 **拷贝到目标目录下。
PS:这里有两个点需要我们关注一下:

项目模板存放位置方式

第一种是和脚手架打包在一起,在安装脚手架的时候就会将项目模板存放在全局目录下了,这种方式每次创建项目的时候都是从本地拷贝的速度很快,但是项目模板自身升级比较困难
第二种是将项目模板存在远端仓库(比如 gitlab 仓库),这种方式每次创建项目的时候都是通过某个地址 **动态下载 **的,项目模板更新方便。
选择第二种,解耦了模板和脚手架,方便更新维护。
代码源码:

#!/usr/bin/env node
const program = require("commander"); // 用于捕获命令
const chalk = require("chalk"); // 用于字体加色
const download = require("download-git-repo"); // 用于下载git的包
const inquirer = require("inquirer"); // 用于与用户输入做交互
const ora = require("ora"); // 进度显示
const symbols = require("log-symbols"); // 信息前面加✔或✖

program
  .version(require("../package.json").version, "-v,--version")
  .command("init <name>")
  .description("初始化项目中...")
  .action((name) => {
    
    
    inquirer
      .prompt([{
    
    
        type: "list", 
        name: "templates", 
        message: "templates:", 
        choices: [{
    
    
          name: "vue",
          value: {
    
    
            gitUrl: "http://***/vue-demo.git",
          }
        },
                  {
    
    
                    name: "react",
                    value: {
    
    
                      gitUrl: "http://***/react-demo.git",
                    }
                  }]
      }])
      .then((answers) => {
    
    
        const {
    
     gitUrl } = answers.templates
        download(
          `${
      
      gitUrl}`,
          `./${
      
      name}`,
          {
    
     clone: true },
          function (err) {
    
    
            if (err) {
    
    
              console.log(err);
            } else {
    
    
              console.log(symbols.success, chalk.green("创建项目成功"));
            }
          }
        );
      });
  });

program.parse(process.argv);

如果下载完项目,需要执行shell命令

node脚本中,执行指定的shell命令

const {
    
     spawn } =  require('child_process')
// 执行终端命令的子线程
const terminal = async (...args) => {
    
    
  return new Promise((resolve) => {
    
    
    // 子线程
    const proc = spawn(...args)
    // 子线程 proc --终端的输出转到--> 主线程 process
    proc.stdout.pipe(process.stdout)
    proc.stderr.pipe(process.stderr)
    proc.on('close', () => {
    
    
      resolve()
    })
  })
}
module.exports = {
    
    
  terminal
}
const install = async(name) => {
    
    
  const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
  await utils.terminal(npm, ['install'], {
    
    cwd: `./${
      
      name}`})
}

// 初始化
module.exports = install

执行进程命令 npm 需要判断 const npm = process.platform === ‘win32’ ? ‘npm.cmd’ : ‘npm’

配置全局 CLI 命令

我们的脚手架开发完成发布到 npm 后,可以通过npm install -g gfe-cli **全局安装 **的方式安装就可以直接使用 CLI 命令了。
但是开发过程中为了 **方便调试和实时同步 **修改,需要另外的方式将 CLI 命令链接到全局。
可以在 gfe-cli 目录下执行 **npm link **,该命令可以将 gfe-cli 下的 bin 命令软链接到全局,直接使用。
Tips:npm link 的时候遇到过几个小坑跟大家分享一下
在开发的过程中可能会遇到 **执行命令失败 **的情况,比如 zsh: command not found: gfe-cli。

  • 尝试重新链接 npm link,再失败的话就尝试先删除掉全局命令 npm unlink gfe-cli 然后再链接,一般情况下这样就可以解决了。
  • 还是不行就去全局目录里删除 gfe-cli 文件夹

发布配置

{
    
    
  "name": "@gfe/cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin":{
    
    
    "gfe-cli": "src/gfe-cli.js"
  },
  "keywords": [
    "tools",
    "javascript",
    "cli"
  ],
  "scripts": {
    
    
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "xcc",
  "license": "ISC"
}

name是包的名字,可以直接写包名,比如cli,或者添加域,类似于@gfe/cli这种,@后面是你npm注册的用户名。key为包的关键字。
.npmrc 文件,发布配置文件

registry=https://*******/
email=****
always-auth=true
_auth=****

.npmignore 文件,忽略上传文件

node_modules/
.babelrc
tsconfig.json
.eslintrc.js

npm publish 发布

开发注意

简述ESM和CJS模块

早期Javascript这门语言是没有模块化的概念的,直到nodejs诞生,才把模块系统引入js。nodejs使用的是CJS(Commonjs)规范,也就是我们平时所见的require、module.exports。而js语言标准的模块规范是ESM(Ecmascript Module),也就是我们在前端工程大量使用的import、export语法。nodejs已经在逐步支持ESM,目前很多主流浏览器也已经原生支持ESM。

项目使用的是ESM还是CJS?

Node.js 8.5.0增加了ESM的实验性支持,使用**–experimental-modules **标识,加上以 **.mjs **为后缀的文件名可以让nodejs执行ESM规范导入导出的模块。例如:

node --experimental-modules index.mjs

Node.js 12.17.0,移除了**–experimental-modules **标识。虽然ESM还是试验性的,但已经相对稳定了。
之后的版本,nodejs按以下流程判断模块系统是用ESM还是CJS:

如何让require 和 import在同一文件中使用

ES6标准发布后,module成为标准,标准的使用是以export指令导出接口,以import引入模块,但是在我们一贯的node模块中,我们采用的是CommonJS规范,使用require引入模块,使用module.exports导出接口
node中通常引入模块是 require 语法,而非 import 语法。 可编写一个js文件 即支持require 语法,又支持import语法

export const funA = () => {
    
    } ; // 函数funA 
export const funB = () => {
    
    } ; // 函数funB

改为

const funA = () => {
    
    } ; // 函数funA 
const funB = () => {
    
    } ; // 函数funB
module.exports = {
    
    
  funA:funA,
  funB:funB,
}

运行 serve.js

const Utils = require('./util')

nodejs原本支持commonjs的模块化规范,就是require这类型的
如果想要使用es6 export import的模块化规范,启动方式:
将文件修改为mjs后缀,或者修改package.json中的 type: module

Rollup打包注意

为何使用Rollup打包工具

Rollup.js是一个模块打包工具,可以帮助你从一个入口文件开始,将所有使用到的模块文件都打包到一个最终的发布文件中(极其适合构建一个工具库,这也是选择用rollup来打包的原因)
Rollup.js有两个重要的特性,其中一个就是它使用ES6的模块标准,这意味着你可以直接使用import和export而不需要引入babel(当然,在现在的项目中,babel可以说是必用的工具了)
Rollup.js一个重要特性叫做’tree-shaking’,这个特性可以帮助你将无用代码(即没有使用到的代码)从最终的生成文件中删去。(这个特性是基于ES6模块的静态分析的,也就是说,只有export而没有import的变量是不会被打包到最终代码中的)

问题一 报错 SyntaxError: Unexpected character ‘!’

入口文件 首行必须添加 #!/usr/bin/env node 用 Rollup 打包报错 可在 rollup.config.js 配置这段代码
rollup配置文件的两个属性:banner和footer,这两个属性会在生成文件的开头和结尾插入一段你自定义的字符串 可新增参数 banner:‘#!/usr/bin/env node’

问题二 报错 [!] Error: ‘default’ is not exported by ****

rollup默认是不支持CommonJS模块的,自己写的时候可以尽量避免使用CommonJS模块的语法,但有些外部库的是cjs或者umd(由webpack打包的),所以使用这些外部库就需要支持CommonJS模块
可引入 rollup-plugin-commonjs 处理

问题三 报错 (!) Unresolved dependencies

  1. 帮助 Rollup 查找外部模块 引入 rollup-plugin-node-resolve
  2. external属性:使用rollup打包,我们在自己的库中需要使用第三方库,例如fs,path等,又不想在最终生成的打包文件中出现fs,path

配置 rollup.config.js

import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import del from 'rollup-plugin-delete'
import {
    
     dependencies } from './package.json'
const external = Object.keys(dependencies || '')
export default {
    
    
  input: './src/gfe-cli.js',
  output: {
    
    
    file: './dist/gfe-cli.mjs',
    banner: '#!/usr/bin/env node'
  },
  external:[...external, 'fs', 'path'],
  plugins: [
    del({
    
    
      targets: 'dist/*'
    }),
    commonjs(),
    resolve({
    
    
      modulesOnly: true,
      preferBuiltins: true
    }),
    json()
  ],
};

npm发布注意

npm 发布私库成功之后,下载找不到地址?

原因:下载未配置私库地址
例如: 脚手架是**@gfe/n-cli**,需要在全局安装 -g
在全局要配置:打开cmd 自动定位到 C:\Users\55409>
在55409文件夹里配置 .npmrc 文件

@gfe:registry=https://****/
registry=https://registry.npm.taobao.org/

@gfe需要对应私库地址 其余内部依赖走npm 公共

猜你喜欢

转载自blog.csdn.net/gaojinbo0531/article/details/129435594