<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手教你從零配置一個(gè)vue組件庫

          共 21445字,需瀏覽 43分鐘

           ·

          2021-08-15 22:01

          來源 | https://www.cnblogs.com/wanglinmantan/p/15092010.html

          簡介

          本文會從零開始配置一個(gè)monorepo類型的組件庫,包括規(guī)范化配置、打包配置、組件庫文檔配置及開發(fā)一些提升效率的腳本等,monorepo 不熟悉的話這里一句話介紹一下,就是在一個(gè)git倉庫里包含多個(gè)獨(dú)立發(fā)布的模塊/包。
          PS:本文涉及到的工具配置其實(shí)在平時(shí)開發(fā)中一般都不需要自己配置,我們使用
          的各種腳手架都幫我們搞定了,但是我們至少得大概知道都是什么意思以及為什么,說來慚愧,筆者作為一個(gè)三四年工齡的前端老人,基本沒有自己動手配過,甚至沒有去了解過,所以以下大部分工具都是筆者第一次使用,除了介紹如何配置也會講到遇到的一些坑及解決方法,另外也會盡量去搞清楚每一個(gè)參數(shù)的意思及原理,有興趣的請繼續(xù)閱讀吧~

          使用lerna管理項(xiàng)目

          首先每個(gè)組件都是一個(gè)獨(dú)立的npm包,但是某個(gè)組件可能又依賴了另一個(gè)組件,這樣如果這個(gè)組件有bug修改完后發(fā)布了新版本,需要手動到依賴它的組件里挨個(gè)進(jìn)行升級再進(jìn)行發(fā)布,這是一個(gè)繁瑣且效率不高的過程,所以可以使用leran工具來進(jìn)行管理,lerna是一個(gè)專門用于管理帶有多個(gè)包的JavaScript項(xiàng)目的工具,可以幫助進(jìn)行npm發(fā)布及git上傳。
          首先全局安裝lerna:
          npm i -g lerna

          然后進(jìn)入倉庫目錄執(zhí)行:

          lerna init

          這個(gè)命令用來創(chuàng)建一個(gè)新的lerna倉庫或者升級一個(gè)現(xiàn)有倉庫的lerna版本,lerna有兩種使用模式:

          1、固定模式,默認(rèn)固定模式下所有包的主版本號和次版本都會使用lerna.json配置里的version字段定義的版本號,如果某一次只修改了其中一個(gè)或幾個(gè)包,但修改了配置文件里的主版本號或次版本號,那么發(fā)布時(shí)所有的包都會統(tǒng)一升級到該版本并進(jìn)行發(fā)布,單個(gè)的包如果想要發(fā)布只能修改修訂版本號進(jìn)行發(fā)布;

          2、獨(dú)立模式就是每個(gè)包使用獨(dú)立的版本號。

          自動生成的目錄如下:

          可以看到?jīng)]有.gitignore文件,所以手動創(chuàng)建一下,目前只需要忽略node_modules目錄。

          我們所有的包都會放在packages文件夾下,添加新包可以使用lerna create xxx命令(后面會通過腳本來生成),組件庫推薦給包名增加一個(gè)統(tǒng)一的作用域scope,可以避免命名沖突,比如常見的@vue/xxx、@babel/xxx等,npm從2.0版本開始支持發(fā)布帶作用域的包,默認(rèn)的作用域是你的npm用戶名,比如:@username/package-name,也可以使用npm config set @scope-name:registry http://reg.example.com 來給你使用的npm倉庫關(guān)聯(lián)一個(gè)作用域。

          給包添加依賴可以使用lerna add module-1 --scope=module-2命令,表示將module-1安裝到module-2的依賴?yán)铮琹earn檢查到如果依賴的包是本項(xiàng)目中的會直接鏈接過去:

          可以看到有個(gè)鏈接標(biāo)志,lerna add默認(rèn)也會執(zhí)行l(wèi)erna bootstrap的操作,即給所有的包安裝依賴項(xiàng)。

          當(dāng)修改完成后需要發(fā)布時(shí)可以使用lerna publish命令,該命令會完成模塊的發(fā)布及git上傳工作,有個(gè)需要注意的點(diǎn)是帶作用域的包使用npm發(fā)布時(shí)需要添加--access public參數(shù),但是lerna publish不支持該參數(shù),一個(gè)解決方法是在所有包的package.json文件里添加:

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

          規(guī)范化配置

          eslint

          eslint是一個(gè)配置化的JavaScript代碼檢查工具,通過該工具可以約束代碼風(fēng)格,以及檢測一些潛在錯(cuò)誤,做到在不同的開發(fā)者下能有一個(gè)統(tǒng)一風(fēng)格的代碼,常見的比如是否允許使用==、語句結(jié)尾是否去掉;等等,eslint的規(guī)則非常多,可以在這里查看https://eslint.bootcss.com/docs/rules/ 。

          eslint的所有規(guī)則都可單獨(dú)配置是否開啟,并且默認(rèn)都是禁用的,所以如果要自己來挨個(gè)配置是比較麻煩的,但是它有個(gè)繼承的配置,可以很方便的使用別人的配置,先來安裝一下:

          npm i eslint --save-dev

          然后在package.json文件里加一個(gè)命令:

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

          之后在命令行輸入npm run lint:init 來創(chuàng)建一個(gè)eslint配置文件,根據(jù)你的情況回答完一些問題后就會生成一個(gè)默認(rèn)配置,我生成的內(nèi)容如下:

          簡單看一下各個(gè)字段的意思:

          • env字段用來指定你代碼所要運(yùn)行的環(huán)境,比如是在瀏覽器環(huán)境下,還是node環(huán)境下,不同的環(huán)境下所對應(yīng)的全局變量不一樣,因?yàn)楹罄m(xù)還要寫node腳本,所以把node:true也加上;

          • parserOptions表示所支持的語言選項(xiàng),比如JavaScript的版本、是否啟用JSX等,設(shè)置正確的語言選項(xiàng)可以讓eslint確定什么是解析錯(cuò)誤;

          • plugins顧名思義是插件列表,比如你使用的是react,那么需要使用react的插件來支持react的語法,因?yàn)槲矣玫氖莢ue,所以使用了vue的插件,可以用來檢測單文件的語法問題,插件的命名規(guī)則為eslint-plugin-xxxx,配置時(shí)前綴可以省略;

          • rules就是規(guī)則配置列表,可以單獨(dú)配置某個(gè)規(guī)則啟用與否;

          • extends就是上文所說的繼承,這里使用了官方推薦的配置以及vue插件順帶提供的配置,配置命名一般為eslint-config-xxx,使用時(shí)前綴也可以省略,并且插件也可以順帶提供配置功能,引入規(guī)則一般為plugin:plugin-name/xxx,此外也可以選擇使用其他一些比較出名的配置如eslint-config-airbnb;

          和.gitignore一樣,eslint也可以創(chuàng)建一個(gè)忽略配置文件.eslintignore,每一行都是一個(gè)glob模式來表示哪些路徑要忽略:

          node_modulesdocsdistassets

          接下來再去package.json文件里加上運(yùn)行檢查的命令:

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

          意思是檢查當(dāng)前目錄下的所有文件,--fix表示允許eslint進(jìn)行修復(fù),但是能修自動復(fù)的問題很少,執(zhí)行npm run lint,結(jié)果如下:

          husky

          目前只能手動去運(yùn)行eslint檢查,就算能約束自己每次提交代碼前檢查一下,也不一定能約束到其他人,沒有強(qiáng)制的規(guī)范和沒有規(guī)范沒啥區(qū)別,所以最好在git提交前采取強(qiáng)制措施,這可以使用Husky,這個(gè)工具可以方便的讓我們在執(zhí)行某個(gè)git命令前先執(zhí)行特定的命令,我們的需求是在git commit之前進(jìn)行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,但是,沒有效果。。。檢查了node、npm和git的版本,均沒有問題,然后我打開git的隱藏文件夾.git/hooks:

          發(fā)現(xiàn)目前的這些鉤子文件后面還是帶著sample后綴,如果想要某個(gè)鉤子生效,這個(gè)后綴要去掉才行,但是這種操作顯然不應(yīng)該讓我手動來干,那么只能重裝husky試試,經(jīng)過簡單的測試,我發(fā)現(xiàn)v5.x版本也是不行的,但是v3.0.0及v1.1.1兩個(gè)版本是生效的(筆者系統(tǒng)是windows10,可能和筆者電腦環(huán)境有關(guān)):

          這樣如果檢查到有錯(cuò)誤就會終止commit操作,不過目前一般還會使用另外一個(gè)包lint-staged,這個(gè)包顧名思義,只檢查staged狀態(tài)下的文件,其他本次提交沒有變動的文件就不用檢查了,這是合理的也能提高檢查速度,先安裝:npm i lint-staged --save-dev,然后去package.json里配置一下:

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

          首先git鉤子執(zhí)行的命令改成lint-staged,lint-staged字段的值是個(gè)對象,對象的key也是glob匹配模式,value可以是字符串或字符串?dāng)?shù)組,每個(gè)字符串代表一個(gè)可執(zhí)行的命令。

          如果lint-staged發(fā)現(xiàn)當(dāng)前存在staged狀態(tài)的文件會進(jìn)行匹配,如果某個(gè)規(guī)則匹配到了文件那么就會執(zhí)行這個(gè)規(guī)則對應(yīng)的命令,在執(zhí)行命令的時(shí)候會把匹配到的文件作為參數(shù)列表傳給此命令。

          比如:exlint --fix xxx.js xxx.vue ...,所以上面配置的意思就是如果在已暫存的文件里匹配到了js或vue文件就執(zhí)行eslint --fix xxx.js ... ,為啥命令不直接寫npm run lint呢,因?yàn)閘int命令里我們配置了./路徑,那么仍將會檢查所有文件。

          執(zhí)行效果如下,在上文的截圖中可以看到一共有14個(gè)錯(cuò)誤,但是本次我只修改了一個(gè)文件,所以只檢查了這一個(gè)文件:

          stylelint

          stylelint和eslint十分相似,只不過是用來檢查css語法的,除了css文件,同時(shí)也支持scss、less等css預(yù)處理語言,stylelint可能沒eslint那么流行,不過本著學(xué)習(xí)的目的,咱們也嘗試一下,畢竟組件庫肯定少不了寫樣式,依舊先安裝:npm i stylelint stylelint-config-standard --save-dev,stylelint-config-standard是推薦的配置文件,和eslint-config-xxx一樣,也可以拿來繼承,不喜歡這個(gè)規(guī)則也可以換其他的,接著創(chuàng)建一個(gè)配置文件.stylelintrc,輸入以下內(nèi)容:

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

          創(chuàng)建一個(gè)忽略配置文件.stylelintignore,輸入:

          node_modules

          最后在package.json中添加一行命令:

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

          檢查packages目錄下所有以css或less結(jié)尾的文件,并且可以的話自動進(jìn)行修復(fù),執(zhí)行命令效果如下:

          最后的最后和eslint一樣,在git commit之前也加上自動進(jìn)行檢查,package.json文件修改如下:

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

          commitlint

          commit的內(nèi)容對于了解一次提交做了什么來說是很重要的,git commit內(nèi)容的標(biāo)準(zhǔn)格式其實(shí)是包含三部分的:Header、Body、Footer,其中Header部分是必填的。

          但是說實(shí)話對于我來說Header部分都懶得認(rèn)真寫,更不用說其他幾部分了,所以靠自覺不行還是上工具吧,讓我們在git的commit-msg鉤子上加上對commit內(nèi)容的檢查功能,不符合規(guī)則就打回重寫,安裝一下校驗(yàn)工具commitlint:

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

          同樣也是一個(gè)工具,一個(gè)配置,通過繼承的方式來使用,嚴(yán)重懷疑這些工具的開發(fā)者都是同一批人,接下來創(chuàng)建一個(gè)配置文件commitlint.config.js,輸入如下內(nèi)容:

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

          當(dāng)然你也可以再單獨(dú)配置你需要的規(guī)則,然后去package.json的husky部分配置鉤子:

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

          commitlint命令需要有輸入?yún)?shù),也就是我們輸入的commit message,-E參數(shù)的意思如下:

          大意就是從環(huán)境變量里給定的文件里獲取輸入內(nèi)容,這個(gè)環(huán)境變量看名字就知道是husky提供的,具體它是啥呢,咱們來簡單看一下。

          首先打開.git/hooks/commit-msg文件,這個(gè)就是commit-msg鉤子執(zhí)行的bash腳本:

          可以看到最后執(zhí)行了run.js,參數(shù)分別為hookName及gitParams,baseName "$0"代表當(dāng)前執(zhí)行的腳本名稱,也就是文件名commit-msg,"$*"代表所有的參數(shù),run.js里又輾轉(zhuǎn)反側(cè)的最后調(diào)用了一個(gè)run方法:

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

          我們打印看一下參數(shù)都是啥:

          可以看到HUSKY_GIT_PARAMS就是一個(gè)文件路徑,這個(gè)文件里保存著我們這次輸入的commit message的內(nèi)容,接著husky會把它設(shè)置到環(huán)境變量里:

          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;}

          現(xiàn)在再看commitlint -E HUSKY_GIT_PARAMS就很容易理解了,commitlint會去讀取.git/COMMIT_EDITMSG文件內(nèi)容來檢查我們輸入的commit message是否符合規(guī)范。

          可以看到我們只輸入了一個(gè)1的話就報(bào)錯(cuò)了。

          commitizen

          上面提到一個(gè)標(biāo)準(zhǔn)的commit message是包含三部分的,詳細(xì)看就是這樣的:

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

          當(dāng)你輸入git commit時(shí),就會出現(xiàn)一個(gè)命令行編輯器讓你來輸入,但是這個(gè)編輯器很不好用,沒用過的話怎么保存都是個(gè)問題,所以可以使用commitizen來進(jìn)行交互式的輸入,依次執(zhí)行下列命令:

          npm install commitizen -g
          commitizen init cz-conventional-changelog --save-dev --save-exact

          執(zhí)行完后應(yīng)該會自動在你的package.json文件里加上下列配置:

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

          然后你就可以使用git cz命令來代替git commit命令了,它會給你一些選項(xiàng),以及詢問你一些問題,如實(shí)輸入即可:

          但是這樣git commit命令仍然是可用的,文檔上說可以進(jìn)行如下配置來將git commit轉(zhuǎn)換為git cz:

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

          但是我嘗試了不行,報(bào)系統(tǒng)找不到指定的路徑。的錯(cuò)誤,沒找到原因和解決方法,如果你知道如何解決的話評論區(qū)見吧~強(qiáng)制不了,那只能加一句卑微的提示了:

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

          規(guī)范化的暫且就配置這么多,其他的比如代碼美化可以使用prettier、生成提交日志的可以使用conventional-changelog或standard-version,有需要的可以自行嘗試。

          打包配置

          目前每個(gè)組件的結(jié)構(gòu)都是類似下面這樣的:

          index.js返回一個(gè)帶install方法的對象,作為vue的插件,使用這個(gè)組件的方式如下:

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

          組件庫其實(shí)直接這么發(fā)布就可以了,如果js文件里使用了最新的語法,那么需要在使用該組件的項(xiàng)目里的vue.config.js里添加一下如下配置:

          {    transpileDependencies: [        'module-x'    ]}

          因?yàn)槟J(rèn)情況下 babel-loader 會忽略所有 node_modules 中的文件,添加這個(gè)配置可以讓Babel 顯式轉(zhuǎn)譯這個(gè)依賴。

          不過如果你硬想要打包后再進(jìn)行發(fā)布也是可以的,我們增加一下打包的配置。

          先安裝一下相關(guān)的工具:

          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

          因?yàn)楸容^多,就不挨個(gè)介紹了,應(yīng)該還是比較清晰的,分別是用來解析樣式文件、vue單文件、js文件及其他文件,可以根據(jù)你的實(shí)際情況增減。

          先說一下打包目標(biāo),分別給每個(gè)包進(jìn)行打包,打包結(jié)果輸出到各自文件夾的dist目錄下,我們使用webpack的node 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')
          // 獲取命令行參數(shù),用來打包指定的包,否則打包packages目錄下的所有包const args = process.argv.slice(2)
          // 生成webpack配置const createConfigList = () => { const pkgPath = path.join(__dirname, '../', 'packages') // 根據(jù)是否傳入了參數(shù)來判斷要打的包 const dirs = args.length > 0 ? args : fs.readdirSync(pkgPath) // 給每個(gè)包生成一個(gè)webpack配置 return dirs.map((item) => { return { // 入口文件為每個(gè)包里的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默認(rèn)打包生成的代碼是包含const、let、箭頭函數(shù)等es6語法的,所以需要設(shè)置一下生成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默認(rèn)使用es module的方式引入圖片,最終生成的鏈接是個(gè)對象,所以如果是通過require方式引入圖片就訪問不了,可以通過該配置關(guān)掉 } } ] }, plugins: [ new VueLoaderPlugin(), new CleanWebpackPlugin() ] } })}
          // 開始打包webpack(createConfigList(), (err, stats) => { // 處理和結(jié)果處理...})

          然后運(yùn)行命令node ./bin/buildModule.js 即可打所有的包,或者node ./bin/buildModule.js xxx xxx2 ...來打你指定的包。

          當(dāng)然,這只是最簡單的配置,實(shí)際上肯定還會遇到很多特定問題,比如:

          • 如果依賴了其他基礎(chǔ)組件庫的話會比較麻煩,推薦這種情況就不要打包了,直接源碼發(fā)布;

          • 尋找文件時(shí)缺少vue擴(kuò)展名,那么需要配置一下webpack的resolve.extensions;

          • 使用了某些比較新的JavaScript語法或者用到j(luò)sx等,那么需要配置一下對應(yīng)的babel插件或預(yù)設(shè);

          • 引用了vue、jquery等外部庫,不可能直接打包進(jìn)去,所以需要配置一下webpack的externals;

          • 某個(gè)包可能有多個(gè)入口,換句話說也就是個(gè)別的包可能有特定的配置,那么可以在該包下面添加一個(gè)配置文件,然后上述生成配置的代碼里可以讀取該文件進(jìn)行配置合并;

          這些問題解決都不難,看一下報(bào)的錯(cuò)然后去搜索一下基本很容易就能解決,有興趣的話也可以去本文的源碼查看。

          接下來做個(gè)小優(yōu)化,因?yàn)閣ebpack打包不是同時(shí)進(jìn)行的,所以包的數(shù)量多了的話總時(shí)間就很慢,可以使用parallel-webpack這個(gè)插件來讓它并行打包:

          npm i parallel-webpack -D

          因?yàn)樗腶pi使用的是配置的文件路徑,不能直接傳遞對象類型,所以需要修改一下上述的代碼,改成導(dǎo)出一個(gè)配置的方式:

          // 文件名改成config.js
          // ...
          // 刪除// webpack(createConfigList(), (err, stats) => { // 處理和結(jié)果處理...// })
          // 增加導(dǎo)出語句module.exports = createConfigList()

          另外創(chuàng)建一個(gè)文件:

          // run.js
          const run = require('parallel-webpack').runconst configPath = require.resolve('./config.js')
          run(configPath, { watch: false, maxRetries: 1, stats: true})

          執(zhí)行node ./bin/run.js即可執(zhí)行,我簡單計(jì)時(shí)了一下,節(jié)省了大約一半的時(shí)間。

          組件文檔配置

          組件文檔工具使用的是VuePress,如果跟我一樣遇到了webpack版本沖突問題,可以選擇在./docs目錄下單獨(dú)安裝:

          cd ./docsnpm initnpm install -D vuepress

          vuepress的基本配置很簡單,使用默認(rèn)主題按照教程配置即可,這里就不細(xì)說了,只說一下如何在文檔里使用packages里的組件,先看一下當(dāng)前目錄結(jié)構(gòu):

          config.js文件是vuepress的默認(rèn)配置文件,打包選項(xiàng)、導(dǎo)航欄、側(cè)邊欄等等都在這里配置,enhanceApp是客戶端應(yīng)用的增強(qiáng),在這里可以獲取到vue實(shí)例,可以做一些應(yīng)用啟動的工作,比如注冊組件等。

          zh/rate是我添加的一個(gè)組件的文檔,文檔及示例內(nèi)容都在文件夾下的README.md文件里,vuepress對markdown做了擴(kuò)展,所以在markdown文件里可以使用像vue單文件一樣包含template、script、style三個(gè)塊,方便在文檔里進(jìn)行示例開發(fā),組件需要先在enhanceApp.js文件里進(jìn)行導(dǎo)入及注冊,那么問題來了,我們是導(dǎo)入開發(fā)中的還是打包后的呢,小朋友才做選擇,成年人都要,比如開發(fā)階段我們就導(dǎo)入開發(fā)中的,開發(fā)完成了就導(dǎo)入打包后的,區(qū)別只是在于package.json里的main入口字段指向不同而已,比如我們先指向開發(fā)中的:

          // package.json
          { "main": "index.js"}

          接下來去enhanceApp.js里導(dǎo)入及注冊:

          import Rate from '@zf/rate'
          export default ({ Vue}) => { Vue.use(Rate)}

          如果直接這樣的話默認(rèn)是會報(bào)錯(cuò)的,因?yàn)檎也坏竭@個(gè)包,此時(shí)我們的包也還沒發(fā)布,所以也不能直接安裝,那怎么辦呢,辦法應(yīng)該有好幾個(gè),比如可以使用npm link來將包鏈接到這里。

          但是這樣太麻煩,所以我選擇修改一下vuepress的webpack配置,讓它尋找包的時(shí)候順便去找packages目錄下找,另外也需要給@zf設(shè)置一下別名,顯然我們的目錄里并沒有@zf,修改webpack的配置需要在config.js文件里操作:

          const path = require('path')
          module.exports = { chainWebpack: (config) => { // 我們包存放的位置 const pkgPath = path.resolve(__dirname, '../../../', 'packages') // 修改webpack的resolve.modules配置,解析模塊時(shí)應(yīng)該搜索的目錄,先去packages,再去node_modules config.resolve .modules .add(pkgPath) .add('node_modules') // 修改別名resolve.alias配置 config.resolve .alias .set('@zf', pkgPath) }}

          這樣在vuepress里就可以正常使用我們的組件了,當(dāng)你開發(fā)完成后就可以把這個(gè)包package.json的入口字段改成打包后的目錄:

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

          其他基本信息、導(dǎo)航欄、側(cè)邊欄等可以根據(jù)你的需求進(jìn)行配置,效果如下:

          使用腳本新增組件

          現(xiàn)在讓我們來看一下新增一個(gè)組件都有哪些步驟:

          1、給要新增的組件取個(gè)名字,然后使用npm search xxx來檢查一下是否已存在,存在就換個(gè)名字;

          2、在packages目錄下創(chuàng)建文件夾,新建幾個(gè)基本文件,通常來說是復(fù)制粘貼其他組件然后修改;

          3、在docs目錄下創(chuàng)建文檔文件夾,新建README.md文件,文件內(nèi)容一般也是通過復(fù)制粘貼;

          4、修改config.js進(jìn)行側(cè)邊欄配置(如果配置了側(cè)邊欄的話)、修改enhanceApp.js導(dǎo)入及注冊組件;

          這一套步驟下來雖然不難,但是繁瑣,很容易漏掉某一步,上述這些事情其實(shí)特別適合讓腳本來干,接下來就實(shí)現(xiàn)一下。

          初始化工作

          先在./bin目錄下新建一個(gè)add.js文件,這個(gè)就是咱們要執(zhí)行的腳本,首先它肯定要接收一些參數(shù),簡單起見這里只需要輸入一個(gè)組件名,但是為了后續(xù)擴(kuò)展方便,我們使用inquirer來處理命令行輸入,接收到輸入的組件名稱后自動進(jìn)行一下是否已存在的校驗(yàn):

          // add.jsconst {    exec} = require('child_process')const inquirer = require('inquirer')const ora = require('ora')// ora是一個(gè)命令行l(wèi)oading工具const scope = '@zf/'// 包的作用域,如果你的包沒有作用域,那么則不需要
          inquirer .prompt([{ type: 'input', name: 'name', message: '請輸入組件名稱', validate(input) { // 異步驗(yàn)證需要調(diào)用這個(gè)方法來告訴inquirer是否校驗(yàn)完成 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 => { // 命令行輸入完成,進(jìn)行其他操作 console.log(answers) }) .catch(error => { // 錯(cuò)誤處理 });

          執(zhí)行后效果如下:

          使用模板創(chuàng)建

          接下來在packages目錄下自動生成文件夾及文件,在【打包配置】一節(jié)中可以看到一個(gè)基本的包一共有四個(gè)文件:index.js、package.json、index.vue以及style.less,首先在./bin目錄下創(chuàng)建一個(gè)template文件夾。

          然后再新建這四個(gè)文件,基本內(nèi)容可以先復(fù)制粘貼進(jìn)去,其中index.js和style.less的內(nèi)容不需要修改,所以直接復(fù)制到新組件的目錄下即可:

          // add.js
          const upperCamelCase = require('uppercamelcase')// 字符串-風(fēng)格的轉(zhuǎn)駝峰const fs = require('fs-extra')
          const templateDir = path.join(__dirname, 'template')// 模板路徑
          // 這個(gè)方法在上述inquirer的then方法里調(diào)用,參數(shù)為命令行輸入的信息const create = ({ name}) => { // 組件目錄 const destDir = path.join(__dirname, '../', 'packages', name) const srcDir = path.join(destDir, 'src') // 創(chuàng)建目錄 fs.ensureDirSync(destDir) fs.ensureDirSync(srcDir) // 復(fù)制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.vue和package.json內(nèi)容的部分信息需要?jiǎng)討B(tài)注入,比如index.vue的組件名、package.json的包名,我們可以使用一個(gè)很簡單的庫json-templater來以雙大括號插值的方法來注入數(shù)據(jù),以package.json為例:

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

          name是我們要注入的數(shù)據(jù),接下來讀取模板的內(nèi)容,然后注入并渲染,最后創(chuàng)建文件:

          // add.js
          const upperCamelCase = require('uppercamelcase')// 字符串-風(fēng)格的轉(zhuǎn)駝峰const render = require('json-templater/string')
          // 渲染模板及創(chuàng)建文件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}) => { // 組件目錄 // ... // 創(chuàng)建package.json renderTemplateAndCreate('package.json', { name: scope + name }, destDir) // index.vue renderTemplateAndCreate('index.vue', { name: upperCamelCase(name) }, srcDir)}

          到這里組件的目錄及文件就創(chuàng)建完成了,文檔的目錄及文件也是一樣,這里就不貼代碼了。

          使用AST修改

          最后要修改的兩個(gè)文件是config.js和enhanceApp.js,這兩個(gè)文件雖然也可以向上面一樣使用模板注入的方式,但是考慮到這兩個(gè)文件修改的頻率可能比較頻繁,所以每次都得去模板里修改不太方便,所以我們換一種方式,使用AST,這樣就不需要模板的占位符了。

          先看enhanceApp.js,每增加一個(gè)組件,我們都需要在這里導(dǎo)入和注冊:

          import Rate from '@zf/rate'
          export default ({ Vue}) => { Vue.use(Rate) console.log(1)}

          思路很簡單,把這個(gè)文件的源代碼先轉(zhuǎn)換成AST,然后在最后一個(gè)import語句后面插入新組件的導(dǎo)入語句,以及在最后一條Vue.use語句和console.log語句之間插入新組件的注冊語句,最后再轉(zhuǎn)換回源碼寫回到這個(gè)文件里,AST相關(guān)的操作可以使用babel的工具包:@babel/parser、@babel/traverse、@babel/generator、@babel/types。

          @babel/parser

          把源代碼轉(zhuǎn)換成AST很簡單:

          // add.jsconst parse = require('@babel/parser').parse
          // 更新enhanceApp.jsconst updateEnhanceApp = ({ name}) => { // 讀取文件內(nèi)容 const filePath = path.join(__dirname, '../', 'docs', 'docs', '.vuepress', 'enhanceApp.js') const code = fs.readFileSync(filePath, { encoding: 'utf-8' }) // 轉(zhuǎn)換成AST const ast = parse(code, { sourceType: "module"// 因?yàn)橛玫搅薫import`語法,所以指明把代碼解析成module模式 }) console.log(ast)}

          生成的數(shù)據(jù)很多,所以命令行一般都顯示不下去,可以去https://astexplorer.net/這個(gè)網(wǎng)站上查看,選擇@babel/parser的解析器即可。

          @babel/traverse

          得到了AST樹之后就需要修改這顆樹,@babel/traverse用來遍歷和修改樹節(jié)點(diǎn),這是整個(gè)過程中相對麻煩的一個(gè)步驟,如果不熟悉AST的基礎(chǔ)知識和操作的話推薦先閱讀一下這篇文檔babel-handbook。

          接下來我們對著上面解析的截圖來寫一下添加import語句的代碼:

          // add.jsconst traverse = require('@babel/traverse').defaultconst t = require("@babel/types")// 這個(gè)包是一個(gè)工具包,用來檢測某個(gè)節(jié)點(diǎn)的類型、創(chuàng)建新節(jié)點(diǎn)等
          const updateEnhanceApp = ({ name}) => { // ...
          // traverse的第一個(gè)參數(shù)是ast對象,第二個(gè)是一個(gè)訪問器,當(dāng)遍歷到某種類型的節(jié)點(diǎn)后會調(diào)用對應(yīng)的函數(shù) traverse(ast, { // 遍歷到了Program節(jié)點(diǎn)會執(zhí)行該函數(shù) // 函數(shù)的第一個(gè)參數(shù)并不是節(jié)點(diǎn)本身,而是代表節(jié)點(diǎn)的路徑,路徑上會包含該節(jié)點(diǎn)和其他節(jié)點(diǎn)之間的關(guān)系信息,后續(xù)的一些操作也都是在路徑上進(jìn)行,如果要訪問節(jié)點(diǎn)本身,可以訪問path.node Program(nodePath) { let bodyNodesList = nodePath.node.body // 通過上圖可以看到是個(gè)數(shù)組 // 遍歷節(jié)點(diǎn)找到最后一個(gè)import節(jié)點(diǎn) let lastImportIndex = -1 for (let i = 0; i < bodyNodesList.length; i++) { if (t.isImportDeclaration(bodyNodesList[i])) { lastImportIndex = i } } // 構(gòu)建即將要插入的import語句的AST節(jié)點(diǎn):import name from @zf/name // 節(jié)點(diǎn)類型及需要的參數(shù)可以在這里查看:https://babeljs.io/docs/en/babel-types // 如果不確定使用哪個(gè)類型的話可以在上述的https://astexplorer.net/網(wǎng)站上看一下某個(gè)語句對應(yīng)的是什么 const newImportNode = t.importDeclaration( [ t.ImportDefaultSpecifier(t.Identifier(upperCamelCase(name))) ], // name t.StringLiteral(scope + name) ) // 當(dāng)前沒有import節(jié)點(diǎn),則在第一個(gè)節(jié)點(diǎn)之前插入import節(jié)點(diǎn) if (lastImportIndex === -1) { let firstPath = nodePath.get('body.0')// 獲取body的第一個(gè)節(jié)點(diǎn)的path firstPath.insertBefore(newImportNode)// 在該節(jié)點(diǎn)之前插入節(jié)點(diǎn) } else { // 當(dāng)前存在import節(jié)點(diǎn),則在最后一個(gè)import節(jié)點(diǎn)之后插入import節(jié)點(diǎn) let lastImportPath = nodePath.get(`body.${lastImportIndex}`) lastImportPath.insertAfter(newImportNode) } } });}

          接下來看一下添加Vue.use的代碼,因?yàn)樯傻腁ST樹太長了,所以不方便截圖,大家可以打開上面的網(wǎng)站輸入示例代碼后看生成的結(jié)構(gòu):

          // add.js
          // ...
          traverse(ast, { Program(nodePath) {},
          // 遍歷到ExportDefaultDeclaration節(jié)點(diǎn) ExportDefaultDeclaration(nodePath) { let bodyNodesList = nodePath.node.declaration.body.body // 找到箭頭函數(shù)節(jié)點(diǎn)的body,目前存在兩個(gè)表達(dá)式節(jié)點(diǎn) // 下面的邏輯和添加import語句的邏輯基本一致,遍歷節(jié)點(diǎn)找到最后一個(gè)vue.use類型的節(jié)點(diǎn),然后添加一個(gè)新節(jié)點(diǎn) let lastIndex = -1 for (let i = 0; i < bodyNodesList.length; i++) { let node = bodyNodesList[i] // 找到vue.use類型的節(jié)點(diǎn) 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 } } // 構(gòu)建新節(jié)點(diǎn):Vue.use(name) const newNode = t.expressionStatement( t.callExpression( t.memberExpression( t.identifier('Vue'), t.identifier('use') ), [ t.identifier(upperCamelCase(name))] ) ) // 插入新節(jié)點(diǎn) if (lastIndex === -1) { if (bodyNodesList.length > 0) { let firstPath = nodePath.get('declaration.body.body.0') firstPath.insertBefore(newNode) } else {// body為空的話需要調(diào)用`body`節(jié)點(diǎn)的pushContainer方法追加節(jié)點(diǎn) 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樹修改完成接下來就可以轉(zhuǎn)回源代碼了:

          //  add.jsconst generate = require('@babel/generator').default
          const updateEnhanceApp = ({ name}) => { // ...
          // 生成源代碼 const newCode = generate(ast)}

          效果如下:

          可以看到使用AST進(jìn)行簡單的操作并不難,關(guān)鍵是要細(xì)心及耐心,找對節(jié)點(diǎn)。另外對config.js的修改也是大同小異,有興趣的可以直接看源碼。

          到這里我們只要使用npm run add命令就可以自動化的創(chuàng)建一個(gè)新組件,可以直接進(jìn)行組件開發(fā)了~

          結(jié)尾

          本文是筆者在改造自己組件庫的一些過程記錄,因?yàn)槭堑谝淮螌?shí)踐,難免會有錯(cuò)誤或不合理的地方,歡迎指出,感謝閱讀,再會~

          示例代碼倉庫:https://github.com/wanglin2/vue_components。


          學(xué)習(xí)更多技能

          請點(diǎn)擊下方公眾號


          瀏覽 47
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看黄色电影 | 我看国产逼 | 四虎影院永久在线 | 欧美一区二区三区成人电影 | 婷婷在线观看av 婷婷在线综合激情 |