关于javascript:万字长文从零配置一个vue组件库

32次阅读

共计 17384 个字符,预计需要花费 44 分钟才能阅读完成。

简介

本文会从零开始配置一个 monorepo 类型的组件库,包含规范化配置、打包配置、组件库文档配置及开发一些晋升效率的脚本等,monorepo 不相熟的话这里一句话介绍一下,就是在一个 git 仓库里蕴含多个独立公布的模块 / 包。

ps. 本文波及到的工具配置其实在平时开发中个别都不须要本人配置,咱们应用的各种脚手架都帮咱们搞定了,然而咱们至多得大略晓得都是什么意思以及为什么,说来惭愧,笔者作为一个三四年工龄的前端老人,根本没有本人入手配过,甚至没有去理解过,所以以下大部分工具都是笔者第一次应用,除了介绍如何配置也会讲到遇到的一些坑及解决办法,另外也会尽量去搞清楚每一个参数的意思及原理,有趣味的请持续浏览吧~

应用 lerna 治理我的项目

首先每个组件都是一个独立的 npm 包,然而某个组件可能又依赖了另一个组件,这样如果这个组件有 bug 批改完后公布了新版本,须要手动到依赖它的组件里挨个进行降级再进行公布,这是一个繁琐且效率不高的过程,所以能够应用 leran 工具来进行治理,lerna是一个专门用于治理带有多个包的 JavaScript 我的项目的工具,能够帮忙进行 npm 公布及 git 上传。

首先全局装置lerna

npm i -g lerna

而后进入仓库目录执行:

lerna init

这个命令用来创立一个新的 lerna 仓库或者降级一个现有仓库的 lerna 版本,lerna有两种应用模式:

1. 固定模式,默认固定模式下所有包的主版本号和次版本都会应用 lerna.json 配置里的 version 字段定义的版本号,如果某一次只批改了其中一个或几个包,但批改了配置文件里的主版本号或次版本号,那么公布时所有的包都会对立降级到该版本并进行公布,单个的包如果想要公布只能批改订正版本号进行公布;

2. 独立模式就是每个包应用独立的版本号。

主动生成的目录如下:

能够看到没有 .gitignore 文件,所以手动创立一下,目前只须要疏忽 node_modules 目录。

咱们所有的包都会放在 packages 文件夹下,增加新包能够应用 lerna create xxx 命令(前面会通过脚本来生成),组件库举荐给包名减少一个对立的作用域 scope,能够防止命名抵触,比方常见的@vue/xxx@babel/xxx 等,npm2.0 版本开始反对公布带作用域的包,默认的作用域是你的 npm 用户名,比方:@username/package-name,也能够应用 npm config set @scope-name:registry http://reg.example.com 来给你应用的 npm 仓库关联一个作用域。

给包增加依赖能够应用 lerna add module-1 --scope=module-2 命令,示意将 module-1 装置到 module-2 的依赖里,learn查看到如果依赖的包是本我的项目中的会间接链接过来:

能够看到有个链接标记,lerna add默认也会执行 lerna bootstrap 的操作,即给所有的包装置依赖项。

当批改实现后须要公布时能够应用 lerna publish 命令,该命令会实现模块的公布及 git 上传工作,有个须要留神的点是带作用域的包应用 npm 公布时须要增加 --access public 参数,然而 lerna publish 不反对该参数,一个解决办法是在所有包的 package.json 文件里增加:

{
    // ...
    "publishConfig": {"access": "publish"}
}

规范化配置

eslint

eslint是一个配置化的 JavaScript 代码查看工具,通过该工具能够束缚代码格调,以及检测一些潜在谬误,做到在不同的开发者下能有一个对立格调的代码,常见的比方是否容许应用 ==、语句结尾是否去掉; 等等,eslint的规定十分多,能够在这里查看 https://eslint.bootcss.com/docs/rules/。

eslint的所有规定都可独自配置是否开启,并且默认都是禁用的,所以如果要本人来挨个配置是比拟麻烦的,然而它有个继承的配置,能够很不便的应用他人的配置,先来装置一下:

npm i eslint --save-dev

而后在 package.json 文件里加一个命令:

{
    "scripts": {"lint:init": "eslint --init"}
}

之后在命令行输出 npm run lint:init 来创立一个 eslint 配置文件,依据你的状况答复完一些问题后就会生成一个默认配置,我生成的内容如下:

简略看一下各个字段的意思:

  • env字段用来指定你代码所要运行的环境,比方是在浏览器环境下,还是 node 环境下,不同的环境下所对应的全局变量不一样,因为后续还要写 node 脚本,所以把 node:true 也加上;
  • parserOptions示意所反对的语言选项,比方 JavaScript 的版本、是否启用 JSX 等,设置正确的语言选项能够让 eslint 确定什么是解析谬误;
  • plugins顾名思义是插件列表,比方你应用的是 react,那么须要应用react 的插件来反对 react 的语法,因为我用的是 vue,所以应用了vue 的插件,能够用来检测单文件的语法问题,插件的命名规定为eslint-plugin-xxxx,配置时前缀能够省略;
  • rules就是规定配置列表,能够独自配置某个规定启用与否;
  • extends就是上文所说的继承,这里应用了官网举荐的配置以及 vue 插件顺带提供的配置,配置命名个别为eslint-config-xxx,应用时前缀也能够省略,并且插件也能够顺带提供配置性能,引入规定个别为plugin:plugin-name/xxx,此外也能够抉择应用其余一些比拟闻名的配置如eslint-config-airbnb

.gitignore 一样,eslint也能够创立一个疏忽配置文件 .eslintignore,每一行都是一个glob 模式来示意哪些门路要疏忽:

node_modules
docs
dist
assets

接下来再去 package.json 文件里加上运行查看的命令:

"scripts": {"lint": "eslint ./ --fix"}

意思是查看当前目录下的所有文件,--fix示意容许 eslint 进行修复,然而能修主动复的问题很少,执行npm run lint,后果如下:

husky

目前只能手动去运行 eslint 查看,就算能束缚本人每次提交代码前检查一下,也不肯定能束缚到其他人,没有强制的标准和没有标准没啥区别,所以最好在 git 提交前采取强制措施,这能够应用 Husky,这个工具能够不便的让咱们在执行某个 git 命令前先执行特定的命令,咱们的需要是在 git commit 之前进行 eslint 查看,这须要应用 pre-commit 钩子,git还有很多其余的钩子:https://git-scm.com/docs/githooks。

国际惯例,先装置:

npm i husky@4 --save-dev

而后在 package.json 文件里增加:

{
    "husky": {
        "hooks": {"pre-commit": "npm run lint"}
    }
}

接着我尝试 git commit,然而,没有成果。。。查看了nodenpmgit的版本,均没有问题,而后我关上 git 的暗藏文件夹.git/hooks

发现目前的这些钩子文件前面还是带着 sample 后缀,如果想要某个钩子失效,这个后缀要去掉才行,然而这种操作显然不应该让我手动来干,那么只能重装 husky 试试,通过简略的测试,我发现 v5.x 版本也是不行的,然而 v3.0.0v1.1.1两个版本是失效的(笔者零碎是 windows10,可能和笔者电脑环境无关):

这样如果查看到有谬误就会终止 commit 操作,不过目前个别还会应用另外一个包 lint-staged,这个包顾名思义,只查看staged 状态下的文件,其余本次提交没有变动的文件就不必查看了,这是正当的也能进步查看速度,先装置:npm i lint-staged --save-dev,而后去 package.json 里配置一下:

{
    "husky": {
        "hooks": {"pre-commit": "lint-staged"}
    },
    "lint-staged": {"*.{js,vue}": ["eslint --fix"]
    }
}

首先 git 钩子执行的命令改成 lint-stagedlint-staged 字段的值是个对象,对象的 key 也是 glob 匹配模式,value能够是字符串或字符串数组,每个字符串代表一个可执行的命令,如果 lint-staged 发现以后存在 staged 状态的文件会进行匹配,如果某个规定匹配到了文件那么就会执行这个规定对应的命令,在执行命令的时候会把匹配到的文件作为参数列表传给此命令,比方:exlint --fix xxx.js xxx.vue ...,所以下面配置的意思就是如果在已暂存的文件里匹配到了 jsvue文件就执行 eslint --fix xxx.js ... ,为啥命令不间接写npm run lint 呢,因为 lint 命令里咱们配置了 ./ 门路,那么仍将会查看所有文件。

执行成果如下,在上文的截图中能够看到一共有 14 个谬误,然而本次我只批改了一个文件,所以只查看了这一个文件:

stylelint

stylelint 和 eslint 十分相似,只不过是用来查看 css 语法的,除了 css 文件,同时也反对 scsslesscss预处理语言,stylelint可能没 eslint 那么风行,不过本着学习的目标,咱们也尝试一下,毕竟组件库必定少不了写款式,仍旧先装置:npm i stylelint stylelint-config-standard --save-devstylelint-config-standard是举荐的配置文件,和 eslint-config-xxx 一样,也能够拿来继承,不喜爱这个规定也能够换其余的,接着创立一个配置文件.stylelintrc,输出以下内容:

{"extends": "stylelint-config-standard"}

创立一个疏忽配置文件.stylelintignore,输出:

node_modules

最初在 package.json 中增加一行命令:

{
    "scripts": {"style:lint": "stylelint packages/**/*.{css,less} --fix"
    }
}

查看 packages 目录下所有以 cssless结尾的文件,并且能够的话主动进行修复,执行命令成果如下:

最初的最初和 eslint 一样,在 git commit 之前也加上主动进行查看,package.json文件批改如下:

{
    "lint-staged": {"*.{css,less}": ["stylelint --fix"]
    }
}

commitlint

commit的内容对于理解一次提交做了什么来说是很重要的,git commit内容的规范格局其实是蕴含三局部的:HeaderBodyFooter,其中 Header 局部是必填的,然而说实话对于我来说 Header 局部都懒得认真写,更不用说其余几局部了,所以靠盲目不行还是上工具吧,让咱们在 gitcommit-msg钩子上加上对 commit 内容的查看性能,不合乎规定就打回重写,装置一下校验工具 commitlint:

npm i --save-dev @commitlint/config-conventional @commitlint/cli

同样也是一个工具,一个配置,通过继承的形式来应用,重大狐疑这些工具的开发者都是同一批人,接下来创立一个配置文件commitlint.config.js,输出如下内容:

module.exports = {extends: ['@commitlint/config-conventional']
}

当然你也能够再独自配置你须要的规定,而后去 package.jsonhusky局部配置钩子:

{
    "husky": {
        "hooks": {"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"}
    }
}

commitlint命令须要有输出参数,也就是咱们输出的 commit message-E 参数的意思如下:

粗心就是从环境变量里给定的文件里获取输出内容,这个环境变量看名字就晓得是 husky 提供的,具体它是啥呢,咱们来简略看一下,首先关上 .git/hooks/commit-msg 文件,这个就是 commit-msg 钩子执行的 bash 脚本:

能够看到最初执行了 run.js,参数别离为hookNamegitParamsbaseName "$0"代表以后执行的脚本名称,也就是文件名 commit-msg"$*" 代表所有的参数,run.js里又辗转反侧的最初调用了一个 run 办法:

function run([, scriptPath, hookName = '', HUSKY_GIT_PARAMS], getStdinFn = get_stdin_1.default) {console.log('拦挡', scriptPath, hookName, HUSKY_GIT_PARAMS)
    
    // ...
}

咱们打印看一下参数都是啥:

能够看到 HUSKY_GIT_PARAMS 就是一个文件门路,这个文件里保留着咱们这次输出的 commit message 的内容,接着 husky 会把它设置到环境变量里:

const env = {};
if (HUSKY_GIT_PARAMS) {env.HUSKY_GIT_PARAMS = HUSKY_GIT_PARAMS;}
if (['pre-push', 'pre-receive', 'post-receive', 'post-rewrite'].includes(hookName)) {
    // Wait for stdin
    env.HUSKY_GIT_STDIN = yield getStdinFn();}
if (command) {console.log(`husky > ${hookName} (node ${process.version})`);
    execa_1.default.shellSync(command, { cwd, env, stdio: 'inherit'});
    return 0;
}

当初再看 commitlint -E HUSKY_GIT_PARAMS 就很容易了解了,commitlint会去读取 .git/COMMIT_EDITMSG 文件内容来查看咱们输出的 commit message 是否符合规范。

能够看到咱们只输出了一个 1 的话就报错了。

commitizen

下面提到一个规范的 commit message 是蕴含三局部的,具体看就是这样的:

<type>(<scope>): <subject>
空行
<body>
空行
<footer>

当你输出 git commit 时,就会呈现一个命令行编辑器让你来输出,然而这个编辑器很不好用,没用过的话怎么保留都是个问题,所以能够应用 commitizen 来进行交互式的输出,顺次执行下列命令:

npm install commitizen -g

commitizen init cz-conventional-changelog --save-dev --save-exact

执行完后应该会主动在你的 package.json 文件里加上下列配置:

{
    "config": {
        "commitizen": {"path": "./node_modules/cz-conventional-changelog"}
    }
}

而后你就能够应用 git cz 命令来代替 git commit 命令了,它会给你一些选项,以及询问你一些问题,如实输出即可:

然而这样 git commit 命令依然是可用的,文档上说能够进行如下配置来将 git commit 转换为git cz

{
    "husky": {
        "hooks": {"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",}
    }
}

然而我尝试了不行,报 零碎找不到指定的门路。的谬误,没找到起因和解决办法,如果你晓得如何解决的话评论区见吧~ 强制不了,那只能加一句低微的提醒了:

{
    "husky": {
        "hooks": {"prepare-commit-msg": "echo ----------------please use [git cz] command instead of [git commit]----------------"
        }
    }
}

规范化的暂且就配置这么多,其余的比方代码丑化能够应用 prettier、生成提交日志的能够应用 conventional-changelog 或 standard-version,有须要的能够自行尝试。

打包配置

目前每个组件的构造都是相似上面这样的:

index.js返回一个带 install 办法的对象,作为 vue 的插件,应用这个组件的形式如下:

import ModuleX from 'module-x'
Vue.use(ModuleX)

组件库其实间接这么公布就能够了,如果 js 文件里应用了最新的语法,那么须要在应用该组件的我的项目里的 vue.config.js 里增加一下如下配置:

{
    transpileDependencies: ['module-x']
}

因为默认状况下 babel-loader 会疏忽所有 node_modules 中的文件,增加这个配置能够让 Babel 显式转译这个依赖。

不过如果你硬想要打包后再进行公布也是能够的,咱们减少一下打包的配置。

先装置一下相干的工具:

npm i webpack less less-loader css-loader style-loader vue-loader vue-template-compiler babel-loader @babel/core @babel/cli @babel/preset-env url-loader clean-webpack-plugin -D

因为比拟多,就不挨个介绍了,应该还是比拟清晰的,别离是用来解析款式文件、vue单文件、js文件及其他文件,能够依据你的理论状况增减。

先说一下打包指标,别离给每个包进行打包,打包后果输入到各自文件夹的 dist 目录下,咱们应用 webpacknode API来做:

// ./bin/buildModule.js

const webpack = require('webpack')
const path = require('path')
const fs = require('fs-extra')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const {VueLoaderPlugin} = require('vue-loader')

// 获取命令行参数,用来打包指定的包,否则打包 packages 目录下的所有包
const args = process.argv.slice(2)

// 生成 webpack 配置
const createConfigList = () => {const pkgPath = path.join(__dirname, '../', 'packages')
    // 依据是否传入了参数来判断要打的包
    const dirs = args.length  > 0 ? args : fs.readdirSync(pkgPath)
    // 给每个包生成一个 webpack 配置
    return dirs.map((item) => {
        return {
            // 入口文件为每个包里的 index.js 文件
            entry: path.join(pkgPath, item, 'index.js'),
            output: {
                filename: 'index.js',
                path: path.resolve(pkgPath, item, 'dist'),// 打包删除到 dist 文件夹下
                library: item,
                libraryTarget: 'umd',// 打包成 umd 模块
                libraryExport: 'default'
            },
            target: ['web', 'es5'],// webpack5 默认打包生成的代码是蕴含 const、let、箭头函数等 es6 语法的,所以须要设置一下生成 es5 的代码
            module: {
                rules: [
                    {
                        test: /\.css$/,
                        use: ['style-loader', 'css-loader']
                    },
                    {
                        test: /\.less$/,
                        use: ['style-loader', 'css-loader', 'less-loader']
                    },
                    {
                        test: /\.vue$/,
                        loader: 'vue-loader'
                    },
                    {
                        test: /\.js$/,
                        loader: 'babel-loader'
                    },
                    {test: /\.(png|jpe?g|gif)$/i,
                        loader: 'url-loader',
                        options: {esModule: false// 最新版本的 file-loader 默认应用 es module 的形式引入图片,最终生成的链接是个对象,所以如果是通过 require 形式引入图片就拜访不了,能够通过该配置关掉}
                    }
                ]
            },
            plugins: [new VueLoaderPlugin(),
                new CleanWebpackPlugin()]
        }
    })
}

// 开始打包
webpack(createConfigList(), (err, stats) => {// 解决和后果解决...})

而后运行命令 node ./bin/buildModule.js 即可打所有的包,或者node ./bin/buildModule.js xxx xxx2 ... 来打你指定的包。

当然,这只是最简略的配置,实际上必定还会遇到很多特定问题,比方:

  • 如果依赖了其余根底组件库的话会比拟麻烦,举荐这种状况就不要打包了,间接源码公布;
  • 寻找文件时短少 vue 扩展名,那么须要配置一下 webpackresolve.extensions
  • 应用了某些比拟新的 JavaScript 语法或者用到 jsx 等,那么须要配置一下对应的 babel 插件或预设;
  • 援用了 vuejquery 等内部库,不可能间接打包进去,所以须要配置一下 webpackexternals
  • 某个包可能有多个入口,换句话说也就是个别的包可能有特定的配置,那么能够在该包上面增加一个配置文件,而后上述生成配置的代码里能够读取该文件进行配置合并;

这些问题解决都不难,看一下报的错而后去搜寻一下根本很容易就能解决,有趣味的话也能够去本文的源码查看。

接下来做个小优化,因为 webpack 打包不是同时进行的,所以包的数量多了的话总工夫就很慢,能够应用 parallel-webpack 这个插件来让它并行打包:

npm i parallel-webpack -D

因为它的 api 应用的是配置的文件门路,不能间接传递对象类型,所以须要批改一下上述的代码,改成导出一个配置的形式:

// 文件名改成 config.js

// ...

// 删除
// webpack(createConfigList(), (err, stats) => {
    // 解决和后果解决...
// })

// 减少导出语句
module.exports = createConfigList()

另外创立一个文件:

// run.js

const run = require('parallel-webpack').run
const configPath = require.resolve('./config.js')

run(configPath, {
    watch: false,
    maxRetries: 1,
    stats: true
})

执行 node ./bin/run.js 即可执行,我简略计时了一下,节俭了大概一半的工夫。

组件文档配置

组件文档工具应用的是 VuePress,如果跟我一样遇到了 webpack 版本抵触问题,能够抉择在 ./docs 目录下独自装置:

cd ./docs
npm init
npm install -D vuepress

vuepress的根本配置很简略,应用默认主题依照教程配置即可,这里就不细说了,只说一下如何在文档里应用 packages 里的组件,先看一下当前目录构造:

config.js文件是 vuepress 的默认配置文件,打包选项、导航栏、侧边栏等等都在这里配置,enhanceApp是客户端利用的加强,在这里能够获取到 vue 实例,能够做一些利用启动的工作,比方注册组件等。

zh/rate是我增加的一个组件的文档,文档及示例内容都在文件夹下的 README.md 文件里,vuepressmarkdown 做了扩大,所以在 markdown 文件里能够应用像 vue 单文件一样蕴含 templatescriptstyle 三个块,不便在文档里进行示例开发,组件须要先在 enhanceApp.js 文件里进行导入及注册,那么问题来了,咱们是导入开发中的还是打包后的呢,小朋友才做抉择,成年人都要,比方开发阶段咱们就导入开发中的,开发实现了就导入打包后的,区别只是在于 package.json 里的 main 入口字段指向不同而已,比方咱们先指向开发中的:

// package.json

{"main": "index.js"}

接下来去 enhanceApp.js 里导入及注册:

import Rate from '@zf/rate'

export default ({Vue}) => {Vue.use(Rate)
}

如果间接这样的话默认是会报错的,因为找不到这个包,此时咱们的包也还没公布,所以也不能间接装置,那怎么办呢,方法应该有好几个,比方能够应用 npm link 来将包链接到这里,然而这样太麻烦,所以我抉择批改一下 vuepresswebpack配置,让它寻找包的时候顺便去找 packages 目录下找,另外也须要给 @zf 设置一下别名,显然咱们的目录里并没有 @zf,批改webpack 的配置须要在 config.js 文件里操作:

const path = require('path')

module.exports = {chainWebpack: (config) => {
        // 咱们包寄存的地位
        const pkgPath = path.resolve(__dirname, '../../../', 'packages')
        // 批改 webpack 的 resolve.modules 配置,解析模块时应该搜寻的目录,先去 packages,再去 node_modules
        config.resolve
            .modules
            .add(pkgPath)
            .add('node_modules')
        // 批改别名 resolve.alias 配置
        config.resolve
            .alias
            .set('@zf', pkgPath)
    }
}

这样在 vuepress 里就能够失常应用咱们的组件了,当你开发实现后就能够把这个包 package.json 的入口字段改成打包后的目录:

// package.json

{"main": "dist/index.js"}

其余根本信息、导航栏、侧边栏等能够依据你的需要进行配置,成果如下:

应用脚本新增组件

当初让咱们来看一下新增一个组件都有哪些步骤:

1. 给要新增的组件取个名字,而后应用 npm search xxx 来检查一下是否已存在,存在就换个名字;

2. 在 packages 目录下创立文件夹,新建几个根本文件,通常来说是复制粘贴其余组件而后批改;

3. 在 docs 目录下创立文档文件夹,新建 README.md 文件,文件内容个别也是通过复制粘贴;

4. 批改 config.js 进行侧边栏配置(如果配置了侧边栏的话)、批改 enhanceApp.js 导入及注册组件;

这一套步骤下来尽管不难,然而繁琐,很容易漏掉某一步,上述这些事件其实特地适宜让脚本来干,接下来就实现一下。

初始化工作

先在 ./bin 目录下新建一个 add.js 文件,这个就是咱们要执行的脚本,首先它必定要接管一些参数,简略起见这里只须要输出一个组件名,然而为了后续扩大不便,咱们应用 inquirer 来解决命令行输出,接管到输出的组件名称后主动进行一下是否已存在的校验:

// add.js
const {exec} = require('child_process')
const inquirer = require('inquirer')
const ora = require('ora')// ora 是一个命令行 loading 工具
const scope = '@zf/'// 包的作用域,如果你的包没有作用域,那么则不须要

inquirer
    .prompt([{
            type: 'input',
            name: 'name',
            message: '请输出组件名称',
            validate(input) {
                // 异步验证须要调用这个办法来通知 inquirer 是否校验实现
                const done = this.async();
                input = String(input).trim()
                if (!input) {return done('请输出组件名称')
                }
                const spinner = ora('正在查看包名是否存在').start()
                exec(`npm search ${scope + input}`, (err, stdout) => {spinner.stop()
                    if (err) {done('查看包名是否存在失败,请重试')
                    } else {if (/No matches/.test(stdout)) {done(null, true)
                        } else {done('该包名已存在,请批改')
                        }
                    }
                })
            }
        }
    ])
    .then(answers => {
        // 命令行输出实现,进行其余操作
        console.log(answers)
    })
    .catch(error => {// 错误处理});

执行后成果如下:

应用模板创立

接下来在 packages 目录下主动生成文件夹及文件,在【打包配置】一节中能够看到一个根本的包一共有四个文件:index.jspackage.jsonindex.vue以及 style.less,首先在./bin 目录下创立一个 template 文件夹,而后再新建这四个文件,根本内容能够先复制粘贴进去,其中 index.jsstyle.less的内容不须要批改,所以间接复制到新组件的目录下即可:

// add.js

const upperCamelCase = require('uppercamelcase')// 字符串 - 格调的转驼峰
const fs = require('fs-extra')

const templateDir = path.join(__dirname, 'template')// 模板门路

// 这个办法在上述 inquirer 的 then 办法里调用,参数为命令行输出的信息
const create = ({name}) => {
    // 组件目录
    const destDir = path.join(__dirname, '../', 'packages', name)
    const srcDir = path.join(destDir, 'src')
    // 创立目录
    fs.ensureDirSync(destDir)
    fs.ensureDirSync(srcDir)
    // 复制 index.js 和 style.less
    fs.copySync(path.join(templateDir, 'index.js'), path.join(destDir, 'index.js'))
    fs.copySync(path.join(templateDir, 'style.less'), path.join(srcDir, 'style.less'))
}

index.vuepackage.json 内容的局部信息须要动静注入,比方 index.vue 的组件名、package.json的包名,咱们能够应用一个很简略的库 json-templater 来以双大括号插值的办法来注入数据,以 package.json 为例:

// ./bin/template/package.json
{"name": "{{name}}",
    "version": "1.0.0",
    "description": "","main":"index.js","scripts": {},"author":"",
    "license": "ISC"
}
  

name是咱们要注入的数据,接下来读取模板的内容,而后注入并渲染,最初创立文件:

// add.js

const upperCamelCase = require('uppercamelcase')// 字符串 - 格调的转驼峰
const render = require('json-templater/string')

// 渲染模板及创立文件
const renderTemplateAndCreate = (file, data = {}, dest) => {const templateContent = fs.readFileSync(path.join(templateDir, file), {encoding: 'utf-8'})
    const fileContent = render(templateContent, data)
    fs.writeFileSync(path.join(dest, file), fileContent, {encoding: 'utf-8'})
}

const create = ({name}) => {
    // 组件目录
    // ...
    // 创立 package.json
    renderTemplateAndCreate('package.json', {name: scope + name}, destDir)
    // index.vue
    renderTemplateAndCreate('index.vue', {name: upperCamelCase(name)
    }, srcDir)
}

到这里组件的目录及文件就创立实现了,文档的目录及文件也是一样,这里就不贴代码了。

应用 AST 批改

最初要批改的两个文件是 config.jsenhanceApp.js,这两个文件尽管也能够向下面一样应用模板注入的形式,然而思考到这两个文件批改的频率可能比拟频繁,所以每次都得去模板里批改不太不便,所以咱们换一种形式,应用AST,这样就不须要模板的占位符了。

先看enhanceApp.js,每减少一个组件,咱们都须要在这里导入和注册:

import Rate from '@zf/rate'

export default ({Vue}) => {Vue.use(Rate)
    console.log(1)
}

思路很简略,把这个文件的源代码先转换成 AST,而后在最初一个import 语句前面插入新组件的导入语句,以及在最初一条 Vue.use 语句和 console.log 语句之间插入新组件的注册语句,最初再转换回源码写回到这个文件里,AST相干的操作能够应用 babel 的工具包:@babel/parser、@babel/traverse、@babel/generator、@babel/types。

@babel/parser

把源代码转换成 AST 很简略:

// add.js
const parse = require('@babel/parser').parse

// 更新 enhanceApp.js
const updateEnhanceApp = ({name}) => {
    // 读取文件内容
    const filePath = path.join(__dirname, '../', 'docs', 'docs', '.vuepress', 'enhanceApp.js')
    const code = fs.readFileSync(filePath, {encoding: 'utf-8'})
    // 转换成 AST
    const ast = parse(code, {sourceType: "module"// 因为用到了 `import` 语法,所以指明把代码解析成 module 模式})
    console.log(ast)
}

生成的数据很多,所以命令行个别都显示不上来,能够去 https://astexplorer.net/ 这个网站上查看,抉择 @babel/parser 的解析器即可。

@babel/traverse

失去了 AST 树之后就须要批改这颗树,@babel/traverse用来遍历和批改树节点,这是整个过程中绝对麻烦的一个步骤,如果不相熟 AST 的基础知识和操作的话举荐先浏览一下这篇文档 babel-handbook。

接下来咱们对着下面解析的截图来写一下增加 import 语句的代码:

// add.js
const traverse = require('@babel/traverse').default
const t = require("@babel/types")// 这个包是一个工具包,用来检测某个节点的类型、创立新节点等

const updateEnhanceApp = ({name}) => {
    // ...
    
    // traverse 的第一个参数是 ast 对象,第二个是一个拜访器,当遍历到某种类型的节点后会调用对应的函数
    traverse(ast, {
        // 遍历到了 Program 节点会执行该函数
        // 函数的第一个参数并不是节点自身,而是代表节点的门路,门路上会蕴含该节点和其余节点之间的关系信息,后续的一些操作也都是在门路上进行,如果要拜访节点自身,能够拜访 path.node
        Program(nodePath) {
            let bodyNodesList = nodePath.node.body // 通过上图能够看到是个数组
            // 遍历节点找到最初一个 import 节点
            let lastImportIndex = -1
            for (let i = 0; i < bodyNodesList.length; i++) {if (t.isImportDeclaration(bodyNodesList[i])) {lastImportIndex = i}
            }
            // 构建行将要插入的 import 语句的 AST 节点:import name from @zf/name
            // 节点类型及须要的参数能够在这里查看:https://babeljs.io/docs/en/babel-types
            // 如果不确定应用哪个类型的话能够在上述的 https://astexplorer.net/ 网站上看一下某个语句对应的是什么
            const newImportNode = t.importDeclaration([ t.ImportDefaultSpecifier(t.Identifier(upperCamelCase(name))) ], // name
                t.StringLiteral(scope + name)
            )
            // 以后没有 import 节点,则在第一个节点之前插入 import 节点
            if (lastImportIndex === -1) {let firstPath = nodePath.get('body.0')// 获取 body 的第一个节点的 path
                firstPath.insertBefore(newImportNode)// 在该节点之前插入节点
            } else { // 以后存在 import 节点,则在最初一个 import 节点之后插入 import 节点
                let lastImportPath = nodePath.get(`body.${lastImportIndex}`)
                lastImportPath.insertAfter(newImportNode)
            }
        }
    });
}

接下来看一下增加 Vue.use 的代码,因为生成的 AST 树太长了,所以不不便截图,大家能够关上下面的网站输出示例代码后看生成的构造:

// add.js

// ...

traverse(ast, {Program(nodePath) {},
    
    // 遍历到 ExportDefaultDeclaration 节点
    ExportDefaultDeclaration(nodePath) {
        let bodyNodesList = nodePath.node.declaration.body.body // 找到箭头函数节点的 body,目前存在两个表达式节点
        // 上面的逻辑和增加 import 语句的逻辑基本一致,遍历节点找到最初一个 vue.use 类型的节点,而后增加一个新节点
        let lastIndex = -1
        for (let i = 0; i < bodyNodesList.length; i++) {let node = bodyNodesList[i]
            // 找到 vue.use 类型的节点
            if (t.isExpressionStatement(node) &&
                t.isCallExpression(node.expression) &&
                t.isMemberExpression(node.expression.callee) &&
                node.expression.callee.object.name === 'Vue' &&
                node.expression.callee.property.name === 'use'
            ) {lastIndex = i}
        }
        // 构建新节点:Vue.use(name)
        const newNode = t.expressionStatement(
            t.callExpression(
                t.memberExpression(t.identifier('Vue'),
                    t.identifier('use')
                ),
                [t.identifier(upperCamelCase(name))]
            )
        )
        // 插入新节点
        if (lastIndex === -1) {if (bodyNodesList.length > 0) {let firstPath = nodePath.get('declaration.body.body.0')
                firstPath.insertBefore(newNode)
            } else {// body 为空的话须要调用 `body` 节点的 pushContainer 办法追加节点
                let bodyPath = nodePath.get('declaration.body')
                bodyPath.pushContainer('body', newNode)
            }
        } else {let lastPath = nodePath.get(`declaration.body.body.${lastIndex}`)
            lastPath.insertAfter(newNode)
        }
    }
});

@babel/generator

AST树批改实现接下来就能够转回源代码了:

//  add.js
const generate = require('@babel/generator').default

const updateEnhanceApp = ({name}) => {
    // ...
    
    // 生成源代码
    const newCode = generate(ast)
}

成果如下:

能够看到应用 AST 进行简略的操作并不难,要害是要仔细及急躁,找对节点。另外对 config.js 的批改也是大同小异,有趣味的能够间接看源码。

到这里咱们只有应用 npm run add 命令就能够自动化的创立一个新组件,能够间接进行组件开发了~

结尾

本文是笔者在革新本人组件库的一些过程记录,因为是第一次实际,难免会有谬误或不合理的中央,欢送指出,感激浏览,再会~

示例代码仓库:https://github.com/wanglin2/vue_components。

正文完
 0