前端脚手架 → 浅析与实践

前置拓展浅析 → 提升前端研发效能

脚手架浅析

简单来说,「前端脚手架」就是指通过指令的方式选择几个选项快速搭建项目基础代码的工具,是伴随着业务复杂度提升而来的提效工具;官方话语是为了保证各施工过程顺利进行而搭建的平台;作为一个熟练的前端工程师,最基本的能力之一就是通过技术选型确定所需要的技术栈,然后根据技术栈选择合适的脚手架工具,来进行项目的初始化;

脚手架本质上是一个操作系统的客户端,它通过命令行执行
脚手架实现流程:

  • 校验文件夹是否存在
    • 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来设置安装依赖的源

脚手架执行原理

  • 在终端输入 vue create vue-test-app
  • 终端解析出 vue 命令
  • 终端在环境变量中找到 vue 命令
  • 终端根据 vue 命令链接到实际文件 vue.js
  • 终端利用 node 执行 vue.js
  • vue.js 解析 command / options
  • vue.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的一个实现,用于执行用户输入的命令
  • CLI(Command-line Interface:命令行界面)是一种基于文本界面(如CMD和Mac终端),用于运行程序
    • CLI是Bash的运行环境,CLI接受用户键盘输入,交给Bash执行,并将程序处理结果以文本的方式进行显示

常用组件库浅析

commander → 命令行指令配置 GitHub

commander是一个Node.js的轻量级命令行框架,用于创建命令行应用程序,其提供了对应用程序命令行告知用户的友好界面,主要负责将命令行参数解析为选项命令参数,为问题显示错误且有对应帮助的系统

  • 快速开始
    • 安装:npm install commander

    • 基本语法:program.command('命令[参数]','命令描述',opts).action(回调函数)

    • 创建脚手架实例

      • CommonJS方式
        const { program } = require('commander')
      • ES方式
      js 复制代码
      import { Command } from 'commander';
      const program = new Command()
      • TS方式
      js 复制代码
      import { Command } from 'commander';
      const program = new Command()
    • 调用方式:链式调用,常见的方法有option、argument、action等方法

      • 定义选项 → option()
        • 每个选项都可以定义一个短选项名称(-后面接一个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分割
        • 解析用户输入的参数可以使用Node提供的process.argv
      • 添加命令 → action()
        • 第一个参数
          • 命令名称,命令参数可以跟在名称后面,也可以用.argument()单独指定。
          • 参数可以必选(尖括号表示)、可选(方括号表示)或变长参数(点表示)
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都是全量发布,无论修改与否
  • 两个基本命令
    • 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:回调函数
  • 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/当前文件夹的符号链接

拓展

npm script浅析

  • npm script 生命周期
    • 检查 scripts 对象中是否存在pretest命令,如果有就先执行该命令
    • 检查是否有xxx命令,如果有就执行,没有就报错
    • 检查 scripts 对象中是否存在posttest命令,如果有就先执行该命令
  • 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变换为并行执行,此时就不会在前置语法校验出错后终止后续执行的情况了,也实现了代码变更的同时给出测试结果和测试运行结果,此时的方式就是将串行中的&&更改为&
      • 通过在末尾加await,我们在任何子命令中启动了长时间运行的进程如mocha--watch,可以通过Ctrl+c来结束进程
    • npm-run-all实现更轻量和简洁的多命令运行

      • 通过加--parallel来实现npm script并行执行
  • npm命令自动补全实现
    • 当不带参数的执行npm run时可以列出scripts对象中定义的所有命令,再结合管道操作符、less(linux)命令就可以实现自动补全

    • npm自动补全工具completion集成到shell中

      • npm completion产生的对应命令放在单独的文件中
      js 复制代码
      npm completion >> ~/.bashrc
      npm completion >> ~/.zshrc
      // >> 的目的是将前面命令的输出追加到后面的文件中
      • .bashrc或者.zshrc中引入这个文件
      bash 复制代码
      echo "[ -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 时补全建议中列出的命令细节

推荐文献

leo:从工程化角度出发的前端脚手架 → 京东
前端脚手架开发入门

相关推荐
GIS程序媛—椰子10 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00116 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端19 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100923 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤439133 分钟前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt