基础概念
随着前端工程化的概念越来越深入人心,脚手架应运而生。简单来说,「前端脚手架」就是指通过选择几个选项快速搭建项目基础代码的工具。前端脚手架可以有效避免我们 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
- 帮助 Rollup 查找外部模块 引入 rollup-plugin-node-resolve
- 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 公共