前置拓展浅析 → 提升前端研发效能
脚手架浅析
简单来说,「前端脚手架」就是指通过指令的方式选择几个选项快速搭建项目基础代码的工具,是伴随着业务复杂度提升而来的提效工具;官方话语是
为了保证各施工过程顺利进行而搭建的平台
;作为一个熟练的前端工程师,最基本的能力之一就是通过技术选型确定所需要的技术栈,然后根据技术栈选择合适的脚手架工具,来进行项目的初始化;
脚手架本质上是一个操作系统的客户端,它通过命令行执行
脚手架实现流程:
- 校验文件夹是否存在
const res = await fs.pathExists(file)
- 删除文件夹
await fs.remove(file)
脚手架开发流程:执行脚手架创建项目 → 开发 → 构建 → 测试 → 部署
背景
- 不使用脚手架时的开发
- 最主要的就是常用的
CV
操作 - 存在的问题
- 重复的工作,繁琐且费时
- CV过来的模版易存在无关的代码
- 不同项目不同团队都有不同的配置,容易忽略相关配置点的添加和更改
- 基础版本不断迭代,版本的管理和相关问题的修复较难
- 最主要的就是常用的
- 使用脚手架时的开发
- 可以快速生成项目的基础代码
- 脚手架的项目模版是经过开发者的严格验证的,也可以说是某类项目的最佳实践,完全可以
较无问题
的使用
脚手架的执行过程浅析
脚手架的常见功能
- 项目创建
- 项目模板新增
- 项目打包发布
- 项目统一测试等
脚手架的内部语法机制
- 以
vue create vue-text-app --force
为例,由三部分构成- 主命令:
vue
- command:
create
- command的param:
vue-test-app
- command的option:用来辅助脚手架在特定场景下用户的选择(可以理解为配置)
- 如在create项目时,可以通过
-r/--registry
来设置安装依赖的源
- 如在create项目时,可以通过
- 主命令:
脚手架执行原理
- 在终端输入
vue create vue-test-app
- 终端解析出
vue
命令 - 终端在环境变量中找到
vue
命令 - 终端根据
vue
命令链接到实际文件vue.js
- 终端利用
node
执行vue.js
vue.js
解析 command / optionsvue.js
执行 command- 执行完毕,退出执行
按需深入解析
全局安装@vue/cli之后会有全局的vue命令
- 终端执行
which vue
,找到vue的实际地址-
which vue
//sudo: /usr/local/bin/vue
-
找到实际的软链地址
cd /usr/local/bin
ll
(source ~/.bash_profile)../lib/node_modules/@vue/cli/bin/vue.js
-
找到软连地址中的
vue.js
文件依赖于首行的#!/usr/bin/env node
#!/usr/bin/env node
:表示在环境变量
中找到node,然后系统使用node去执行这个JS文件#!/usr/bin/node
- 调用系统环境变量中的解释器执行文件(调用
/usr/bin
下的node解释器) #!/usr/bin/env node
这是另一种写法,当上述的解释器找不到时(系统用户没有将node装到默认的/usr/bin路径下,相当于是写死了node路径),就会报错,因此一般会采用这个(这种方式会在环境变量里寻找node目录,推荐使用)
- 调用系统环境变量中的解释器执行文件(调用
- 也是
为啥可以直接通过vue命令直接执行JS文件的原因
-
返回到上一级后查看
package.json
中的bin
字段配置js"bin": { "vue": "bin/vue.js" },
- 解释了
全局安装@vue/cli之后会有全局的vue命令
的问题
- 解释了
-
通过npm API获取package信息
获取package最新版本
js
import urlJoin from 'url-join'
import axios from 'axios'
import { log } from '@lbxin.com/utils'
function getNpmInfo(npmName){
// https://registry.npmjs.org npm源
// const registry = 'https://registry.npm.taobao.org'
const registry = 'https://registry.npmjs.org'
const url = urlJoin(registry,npmName)
return axios.get(url).then(({data}) => {
try {
return data
} catch (error) {
return Promise.reject(error)
}
// if(res.status === 200) {
// return res.data
// } else {
// return Promise.reject()
// }
})
}
function getLatestVersion(npmName){
return getNpmInfo(npmName).then(data => {
if(!data['dist-tags'] || !data['dist-tags'].latest){
log.error('没有 latest 版本号')
return Promise.reject(new Error('没有 latest 版本号'))
}
return data['dist-tags'].latest
})
}
// 获取最新的版本号
const latestVersion = await getLatestVersion(selectedTemplate.npmName)
log.verbose(selectedTemplate.version,'版本号存在最新,升级后版本号为:',latestVersion)
latestVersion !== selectedTemplate.version && (selectedTemplate.version === latestVersion)
开发命令行交互列表
Bash、CLI和Shell浅析
- Shell是计算机提供给用户与其他程序员进行交互的接口,其是一个命令解析器,当输入命令后,由Shell进行编译后交给操作系统内核进行处理,例如图形操作系统就是Shell,其属于GUI Shell;
- 是操作系统提供的接口程序,用于
接收用户输入的命令
,交给系统内核进行接收并相应结果
- 是操作系统提供的接口程序,用于
- Bash是一种程序,用于进行人机交互的,Bash与其他程序的最大区别在于其不是用来完成特定的任务,而是用来通过bash shell来执行程序;
- Bash是shell的一个实现,用于
执行用户输入的命令
- Bash是shell的一个实现,用于
- CLI(Command-line Interface:命令行界面)是一种基于文本界面(如CMD和Mac终端),用于运行程序
- CLI是Bash的
运行环境
,CLI接受用户键盘输入,交给Bash执行,并将程序处理结果以文本的方式进行显示
- CLI是Bash的
常用组件库浅析
commander → 命令行指令配置 GitHub
commander是一个Node.js的轻量级命令行框架,用于创建命令行应用程序,其提供了对应用程序命令行告知用户的友好界面,主要负责
将命令行参数
解析为选项
和命令参数
,为问题显示错误且有对应帮助的系统
- 快速开始
-
安装:
npm install commander
-
基本语法:
program.command('命令[参数]','命令描述',opts).action(回调函数)
-
创建脚手架实例
- CommonJS方式
const { program } = require('commander')
- ES方式
jsimport { Command } from 'commander'; const program = new Command()
- TS方式
jsimport { Command } from 'commander'; const program = new Command()
- CommonJS方式
-
调用方式:链式调用,常见的方法有option、argument、action等方法
- 定义选项 → option()
- 每个选项都可以定义一个短选项名称(-后面接一个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或
|
分割 - 解析用户输入的参数可以使用Node提供的
process.argv
- 每个选项都可以定义一个短选项名称(-后面接一个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或
- 添加命令 → action()
- 第一个参数
- 命令名称,命令参数可以跟在名称后面,也可以用.argument()单独指定。
- 参数可以必选(尖括号表示)、可选(方括号表示)或变长参数(点表示)
- 第一个参数
- 定义选项 → option()
-
inquirer:命令行交互工具 GitHub
inquirer是一个提供了命令行的交互操作的库,其简化了询问终端用户的问题、解析、验证答案、提供错误反馈等功能,通俗来说就是帮助我们实现与用户的交互式交流
- 基本方法API
- 启动提示界面(查询会话)
inquirer.prompt(questions, answers) -> promise
- 注册一个提问类型插件:
inquirer.registerPrompt(name, prompt)
- 创建一个独立的查询模块:
inquirer.createPromptModule() -> prompt function
- 启动提示界面(查询会话)
- 为每个问题提供的常用参数
- type:表示提问的类型,包括:input、confirm、list、rawlist、expand、checkbox、password、editor;
- name:存储当前问题回答的变量
- message:问题描述
- default:默认值
- choices:选项列表,在某些type下使用,并且包含一个分隔符(separator)
- validate:对用户的回答进行校验
- filter:对用户的回答进行过滤处理,返回处理后的值
- pageSize:修改某些type类型下的渲染行数
- prefix:修改message默认前缀
- suffix:修改message默认后缀
- transformer:对用户回答的显示效果进行处理,不会影响最终答案内容(如修改背景色、字体等)
- when:根据前面回答,判断当前问题是否需要被回答
js
// project/lbxin-cli/packages-esm/utils/lib/inquirer.js
import inquirer from 'inquirer'
function make({
choices,
defaultValue,
message = '请选择',
type = 'list',
require = true,
mask = '*',
validate,
pageSize,
loop
}) {
// 用于创建默认的inquirer
const options = {
name: 'name',
default: defaultValue,
message,
type,
require,
mask,
validate,
pageSize,
loop
}
type === 'list' && (options.choices = choices)
return inquirer.prompt(options).then(answer => answer.name)
}
export function makeList(params) {
// List选择封装
return make({...params})
}
export function makeInput(params) {
// 输入内容封装
return make({
type: 'input',
...params
})
}
js
// project/lbxin-cli/packages-esm/init/lib/createTemplate.js
const ADD_TYPE = [
{
name: "项目",
value: ADD_TYPE_PROJECT
},
{
name: "页面",
value: ADD_TYPE_PAGE
}
]
// 获取创建类型
function getAddType() {
return makeList({
choices: ADD_TYPE,
message: "请选择初始化类型",
defaultValue: ADD_TYPE_PROJECT,
})
}
// 获取项目名称
function getAddName() {
return makeInput({
message: "请输入项目名称",
defaultValue: "",
// validate 可以实现校验用户输入的信息 返回true时通过 返回false或字符串时不通过
validate(v){
if(v.length > 0){
return true
} else {
return '项目名称不能为空'
}
}
})
}
ora:终端Loading美化工具 GitHub
用于显示加载中的效果,类似于前端页面中的Loading效果;
js
import ora from 'ora'
function getCacheDir(targetPath){
return path.resolve(targetPath,'node_modules')
}
function makeCacheDir(targetPath){
const cacheDir = getCacheDir(targetPath) //得到缓存目录
if(!pathExistsSync(cacheDir)){
// 创建目录
fse.mkdirpSync(cacheDir)
}
}
function downloadTemplate(selectedTemplate){
const {targetPath, template} = selectedTemplate
makeCacheDir(targetPath)
// 安装的目录是在`/Users/qihoo/.lbxin-cli/addTemplate`目录下 并不是在`/Users/qihoo/.lbxin-cli/addTemplate/node_modules`
// 进度条显示 利用ora库
const spinner = ora('正在下载模版...').start()
// try cache捕获下载结果
try {
// setTimeout(() => {
// spinner.stop()
// log.success('下载模板成功~')
// }, 2000);
await downloadAddTemplate(targetPath,template)
spinner.stop()
log.success('下载模板成功')
} catch (error) {
spinner.stop()
// log.error(error.message)
// 不进行普通打印 需要加入`--debug`的校验和输出
printErrorLog(error)
}
}
chalk:命令行输入、输出美化工具 GitHub
- 调用方式:链式调用
- 语法:
chalk.<style>[.<style>...](string, [string...])
- 示例: chalk.red.bold.underline('Hello', 'world');【链式调用】
js
/**
* 通过Node chalk.mjs进行运行
* .mjs是为了解决Node的require失效的问题 chalk不支持require语法 chalk默认支持ES Module (export{}或export default XXX )
*/
console.log('\x1B[41m\x1B[4m\x1B[0m%s','your name')
console.log('\x1B[4B\x1B[4G%s','your name2')
console.log('\x1B[6G%s','your name3')
// const chalk = require('chalk')
import chalk,{Chalk} from 'chalk'
console.log(chalk.red('hello lbxin')+'!')
console.log(chalk.red('hello lbxin')+' ! '+chalk.blue('welcome chalk'))
console.log(chalk.red.bgGreen.bold('hello lbxin')+'!')
console.log(chalk.red.italic.bold('hello','lbxin')+'!')
console.log(chalk.red('hello',chalk.bold.underline('lbxin'))+'!')
console.log(chalk.rgb(255,255,0).italic('hello lbxin')+'!') //自定义色值
console.log(chalk.hex('#ffff00').bold('hello lbxin')+'!') //自定义色值
// 配置化展示error等信息
const error = (...text) => console.log(chalk.bold.hex('#ff0000')(text))
const warning = (...text) => console.log(chalk.bold.hex('#ffa500')(text))
error('error!')
warning('warning!')
lerna深入浅析
作为项目管理工具,旨在优化基于git+npm的多package项目的包管理方式
- 作为一种多包依赖解决方案。lerna的特点如下
- 可以管理公共依赖和单独依赖
- 多package相互依赖直接内部link,不必发版
- 支持项目的单独发版和全体发布
- 多包放一个git仓库,利于代码管理
- 两种工作模式
- Independent模式
- 该模式下,lerna会配合Git,检查文件变动,只发布有改动的包
- Fixed/Locked模式(默认)
- 该模式下,lerna会将工程当做是一个整体来对待,每次发布packages都是全量发布,无论修改与否
- Independent模式
- 两个基本命令
- lerna bootstrap:将库中的依赖联系起来
- lerna publish:将发布任何更新的包
在未使用lerna之前,想要调试一个本地的npm包,需要使用npm link来进行调试,但是在lerna中可以直接进行模块的引入和调试,从而实现了
动态创建软链
;
node中如何实现软链(lerna也是同样的实现方式)
fs.symlinkSync(target,path,type)
- 创建名为path的链接,该链接指向target
- target
<string> | <Buffer> | <URL>
// 目标文件 - path
<string> | <Buffer> | <URL>
// 创建软链对应的地址 - type
<string>
- 仅在windows上可用,其他平台会被忽略
- 可以被设置为'dir'、'file'或'junction'
- windows上的额连接点要求目标路径是绝对路径,当使用
junction
时,target参数将自动的标准化为绝对路径
其他辅助工具
- ncp:像cp -r一样拷贝目录、文件
- download-git-repo:从GitHub/gitlab中拉取仓库代码
- download(repository, destination, options, callback)
- repository:远程仓库地址
- destination:下载到本地的路径
- options:配置参数(若传入函数,则覆盖回调函数)
- callback:回调函数
- download(repository, destination, options, callback)
- axios:结合gitlab API获取仓库列表、Tags、版本号等 -
npm install chalk commander download-git-repo inquirer ora --save
lerna用于项目管理工具,目前像Babel、vue-cli、create-react-app等大型项目都采用lerna进行管理,其优势在于:
- 大幅减少重复操作:多个package时的本地link、单元测试、代码提交、代码发布,可以通过lerna一键操作
- 提升操作的标准化:多个package时的发布版本和相互依赖都可以通过lerna保持一致
拓展
package.json 拓展
bin字段浅析
常规运行Node文件,可以直接使用node/nodemon+文件路径,但是在脚手架中这样是指定不行滴,因此就需要借助其他方式进行实现,package中的bin字段就是实现这个功能的,将bin的字段命令映射到本地文件名,npm将软连接这个文件到prefix/bin里面或者在./node_modules/.bin;但是需要必须打包成全局包才可以使用该命令;
除了将项目发包再global安装外,也可以在项目根目录下执行npm link
命令将指定的执行的文件链接到全局环境
- npm link 和 npm unlink浅析
- npm link:将当前项目链接到node全局node_modules中作为一个库文件,并解析bin配置创建可执行文件
- npm link your-pro:将当前项目中的node_modules下指定的库文件链接到node全局node_modules下的库文件
- npm unlink:将当前项目从node全局node_modules中移除
- npm unlink your-pro:将当前项目中库文件下的指定库链接移除
- 打包成全局的命令
- npm install . -g
- npm link
- 在安装第三方有bin字段的npm时,那些可执行的文件会被连接到当前项目的
./node_modules/.bin
中 - npm link程序包链接的过程中一共两个步骤
- 将在全局文件夹
{prefix}/lib/node_modules/<package>
中创建一个符号链接,该链接连接到npm link
执行命令的包 - 其他位置,执行
npm link package-name
将创建一个从全局安装package-name到node_modules/当前文件夹的符号链接
- 将在全局文件夹
- 在安装第三方有bin字段的npm时,那些可执行的文件会被连接到当前项目的
拓展
npm script浅析
- npm script 生命周期
- 检查 scripts 对象中是否存在
pretest
命令,如果有就先执行该命令 - 检查是否有
xxx
命令,如果有就执行,没有就报错 - 检查 scripts 对象中是否存在
posttest
命令,如果有就先执行该命令
- 检查 scripts 对象中是否存在
- npm run xxx的基本步骤
- 从
package.json
文件中读取scripts对象里的全部配置 - 以传入run的第一个参数为键,获取到对应的值后作为接下来要执行的命令,如果没找到直接报错
- 在系统默认的shell中执行上述命令,系统默认的shell通常是bash、windows下略微不同
- 内部逻辑浅析
- 在执行对应找到的值命令时,npm在执行scripts之前会将node_modules/.bin加到环境变量$path的前面,这也就意味着任何内涵可执行文件的npm以来都可以在npm script中直接调用,即不需要再配置scripts时指定完整的路径
- 从
- npm 多命令姿势
-
通常来说,前端项目会有很多格式的文件,为了保证质量,给不同类型的代码添加检查是很有必要的,代码检查不仅保障代码没有低级的语法错误,还可以确保都遵守社区的最佳实践和一致的编码风格;当然给代码添加单元测试也是质量保障的重要手段
-
串行执行npm script
- 在运行某些命令前需要保证其他校验是通过的,如在执行单元测试前保证代码检查已通过校验是合理的;此时只需要将多个script命令通过
&&
符号连接即可
js// ... "scripts": { "lint:js": "eslint *.js", "lint:css": "stylelint *.less", "lint:json": "jsonlint --quiet *.json", "lint:markdown": "markdownlint --config .markdownlint.json *.md", // "test": "mocha tests/", "test": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/" } // ...
- 在运行某些命令前需要保证其他校验是通过的,如在执行单元测试前保证代码检查已通过校验是合理的;此时只需要将多个script命令通过
-
多命令并行
- 有时候需要将上述的串行执行script变换为并行执行,此时就不会在前置语法校验出错后终止后续执行的情况了,也实现了代码变更的同时给出测试结果和测试运行结果,此时的方式就是将串行中的
&&
更改为&
; - 通过在末尾加
await
,我们在任何子命令中启动了长时间运行的进程如mocha
的--watch
,可以通过Ctrl+c
来结束进程
- 有时候需要将上述的串行执行script变换为并行执行,此时就不会在前置语法校验出错后终止后续执行的情况了,也实现了代码变更的同时给出测试结果和测试运行结果,此时的方式就是将串行中的
-
npm-run-all实现更轻量和简洁的多命令运行
- 通过加
--parallel
来实现npm script并行执行
- 通过加
-
- npm命令自动补全实现
-
当不带参数的执行
npm run
时可以列出scripts对象中定义的所有命令,再结合管道操作符、less(linux)命令就可以实现自动补全 -
npm自动补全工具
completion
集成到shell中- 将
npm completion
产生的对应命令放在单独的文件中
jsnpm completion >> ~/.bashrc npm completion >> ~/.zshrc // >> 的目的是将前面命令的输出追加到后面的文件中
- 在
.bashrc
或者.zshrc
中引入这个文件
bashecho "[ -f ~/.npm-completion.bash ] && source ~/.npm-completion.bash;" >> ~/.bashrc echo "[ -f ~/.npm-completion.bash ] && source ~/.npm-completion.bash;" >> ~/.zshrc
- 📢:执行完上面的命令一定要记得
source ~/.zshrc
或者source ~/.bashrc
,来让自动完成生效。 - 📢:
npm completion
可以实现的不仅仅是scripts
里配置的子命令,npm官方的子命令也是可以的
- 将
-
拓展
- zsh-better-npm-completion实现自动补全的同时拓展其他功能
- 可以在npm install时自动根据历史安装过的包给出补全建议
- 可以在npm uninstall时根据package.json里面的声明给出补全建议
- 可以在npm run 时补全建议中列出的命令细节
- zsh-better-npm-completion实现自动补全的同时拓展其他功能
-