rollup開發(fā)依賴包(npm library)實戰(zhàn)

本文涉及包版本:node 11.6.0 、npm 6.11.3汰瘫、webpack 4.39.3狂打;使用mac開發(fā);

項目

源碼 -> https://github.com/jiaoyanlin/npm-library-demo混弥,求star??

npm i
npm start

建議開始動手實踐前先瀏覽下本文的的 三趴乡、知識點

一、發(fā)布包基本流程

1蝗拿、使用nrm管理npm源:

nrm:npm registry 管理工具晾捏,方便切換不同的源;我們開發(fā)的包要發(fā)布的源是https://registry.npmjs.org哀托,更詳細的安裝可以參考nrm —— 快速切換 NPM 源

// 安裝
npm install -g nrm
// 查看
nrm ls
// 切換
nrm use taobao
// 增加源
nrm add  <registry> <url> [home]
// 刪除源
nrm del <registry>

2惦辛、發(fā)布包:

記得先在 https://www.npmjs.com 注冊賬戶并在郵箱激活賬戶

(1)編寫包代碼(npm init等操作,具體在下面會提及)

(2)切換registry到npm對應鏈接https://registry.npmjs.org/:nrm use npm

(3)登錄:npm login

(4)發(fā)布仓手、更新:npm publish

3胖齐、關(guān)于為何選擇rollup而不是webpack編寫一個npm包

為了支持tree shaking,得導出一份符合es6模塊規(guī)范的代碼嗽冒,但是webpack不支持導出為es6模塊呀伙,所以使用rollup來開發(fā)我們的包

rollup和webpack使用場景分析中提到:Rollup偏向應用于js庫,webpack偏向應用于前端工程添坊,UI庫剿另;如果你的應用場景中只是js代碼,希望做ES轉(zhuǎn)換,模塊解析驰弄,可以使用Rollup麻汰。如果你的場景中涉及到css、html戚篙,涉及到復雜的代碼拆分合并五鲫,建議使用webpack。

rollup可以直接構(gòu)建出符合es6模塊規(guī)范的代碼(有利于tree shaking)岔擂,但是webpack不能位喂;因此為了更好地使用es6模塊化來實現(xiàn)tree shaking,以及優(yōu)化包代碼體積等原因乱灵,選用rollup來開發(fā)npm包塑崖;

二、使用rollup構(gòu)建npm包

以下內(nèi)容引自rollup中文網(wǎng): 為了確保你的 ES6 模塊可以直接與「運行在 CommonJS(例如 Node.js 和 webpack)中的工具(tool)」使用痛倚,你可以使用 Rollup 編譯為 UMD 或 CommonJS 格式规婆,然后在 package.json 文件的 main 屬性中指向當前編譯的版本。如果你的 package.json 也具有 module 字段蝉稳,像 Rollup 和 webpack 2 這樣的 ES6 感知工具(ES6-aware tools)將會直接導入 ES6 模塊版本抒蚜。

關(guān)于rollup更加詳細的介紹及使用,可以參考以下文章:Rollup:下一代ES模塊打包工具 耘戚、rollup中文網(wǎng) 嗡髓、Rollup.js 實戰(zhàn)學習筆記webpack創(chuàng)建library

1收津、先來個簡單的demo:源碼

(1)新建一個文件夾npm-library-demo

初始化:

cd npm-library-demo
npm init -y // 初始化饿这,生成package.json
npm i rollup -D // 安裝rollup

根據(jù)以下目錄結(jié)構(gòu)新增文件夾及文件:

npm-library-demo
    |--build
        |--rollup.config.js
    |--example
        |--index.html
    |--src
        |--main.js
        |--foo.js

(2)文件內(nèi)容:

package.json中加入構(gòu)建腳本命令:

"scripts": {
    "build": "rollup -c ./build/rollup.config.js"
}
// rollup.config.js
const path = require('path');
const resolve = function (filePath) {
    return path.join(__dirname, '..', filePath)
}
export default {
    input: resolve('src/main.js'), // 入口文件
    output: { // 出口文件
        file: resolve('dist/bundle.js'),
        format: 'umd',
        name: 'myLibrary'
    }
};
// main.js
import foo from './foo.js';
export default (function () {
    console.log(foo);
})();
// foo.js
export default 'hello world!';

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>example</title>
</head>
<body>
    
</body>
<script src="../dist/bundle.js"></script>
</html>

(3)執(zhí)行npm run build,就可以生成打包文件/dist/bundle.js撞秋,打開example/index.html控制臺可以查看打包文件是否生效

2长捧、使用插件,在1的基礎(chǔ)上進行以下操作:源碼

在rollup中如果要處理json部服,就要用到插件唆姐,比如rollup-plugin-json

npm i rollup-plugin-json -D

// rollup.config.js
const path = require('path');
import json from 'rollup-plugin-json';

const resolve = function (filePath) {
    return path.join(__dirname, '..', filePath)
}

export default {
    input: resolve('src/main.js'),
    output: {
        file: resolve('dist/bundle.js'),
        format: 'umd',
        name: 'myLibrary'
    },
    plugins: [ // 在此處使用插件
        json(),
    ],
};

// main.js
import foo from './foo.js';
import { version } from '../package.json'; // 利用json插件可以獲得package.json中的數(shù)據(jù)
console.log('version ' + version);
export default (function () {
    console.log(foo);
})();

此時再次使用npm run build打包,打開index.html廓八,在控制臺可以看到相關(guān)結(jié)果

其他插件使用方式類似

3奉芦、Rollup 與其他工具集成

(1)npm packages:添加配置讓rollup知道如何處理你從npm安裝到node_modules文件夾中的軟件包

npm install rollup-plugin-node-resolve rollup-plugin-commonjs -D

  • rollup-plugin-node-resolve: 告訴 Rollup 如何查找外部模塊
  • rollup-plugin-commonjs:將CommonJS模塊轉(zhuǎn)換為 ES2015 供 Rollup 處理,請注意剧蹂,rollup-plugin-commonjs應該用在其他插件轉(zhuǎn)換你的模塊之前 - 這是為了防止其他插件的改變破壞CommonJS的檢測
// rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';

import json from 'rollup-plugin-json';

const path = require('path');
const resolveFile = function (filePath) {
    return path.join(__dirname, '..', filePath)
}

export default {
    input: resolveFile('src/main.js'),
    output: {
        file: resolveFile('dist/bundle.js'),
        format: 'cjs',
    },
    plugins: [
        commonjs(),
        resolve({
            // 將自定義選項傳遞給解析插件
            customResolveOptions: {
                moduleDirectory: 'node_modules'
            }
        }),
        json(),
    ],
};
(2)external:有些包要處理成外部引用(例如lodash等)声功,externals就是用來處理外部的引用,不要將這些包打包到輸出文件中宠叼,減小打包文件體積

external 接受一個模塊名稱的數(shù)組或一個接受模塊名稱的函數(shù)先巴,如果它被視為外部引用(externals)則返回true

// rollup.config.js
export default {
    ...,
    // 作用:指出應將哪些模塊視為外部模塊其爵,否則會被打包進最終的代碼里
    external: ['lodash']
    // external: id => /lodash/.test(id) // 也可以使用這種方式
};

安裝lodash:npm i lodash -S

// main.js
...
import _ from 'lodash';
console.log('-------lodash:', _.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 }));

可以打包試試external配置與否對打包文件的影響(直接查看dist/bundle.js)

由于此時打包生成的是cjs格式的js,可以直接在控制臺執(zhí)行node ./dist/bundle.js測試打包結(jié)果伸蚯;此時index.html是沒法成功加載bundle.js的摩渺,因為此時的文件是cjs的,無法直接在瀏覽器中使用

(3)babel7:

npm i -D rollup-plugin-babel @babel/core @babel/plugin-transform-runtime @babel/preset-env

npm i -S @babel/runtime @babel/runtime-corejs2

// rollup.config.js
...
import babel from 'rollup-plugin-babel';

export default {
    ...
    plugins: [
        ...,
        babel({
            exclude: 'node_modules/**', // 只編譯我們的源代碼
            runtimeHelpers: true,
        }),
    ],
    external: id => {
        return /@babel\/runtime/.test(id) || /lodash/.test(id);
    }
}

根目錄下新建文件.babelrc.js

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                // "debug": true, // debug剂邮,編譯的時候 console
                "useBuiltIns": false, // 是否開啟自動支持 polyfill
                "modules": false, // 模塊使用 es modules 摇幻,不使用 commonJS 規(guī)范
                // "targets": "> 0.25%, last 2 versions, iOS >= 8, Android >= 4.4, not dead"
            }
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                // useESModules:引入的helpers是否是es modules規(guī)范的;注意當打包成cjs時不能引入es modules下的代碼挥萌,會報錯
                // "useESModules": true,
                "corejs": 2 // 參考官方文檔
            }
        ],
    ]
}

可以自己在main.js中加入一些es6語法绰姻,看看打包后的文件是否將es6語法編譯成了es5(如const、let等)

??????? babel還有一篇相關(guān)博文補充

(4)引入eslint:

npm i -D babel-eslint rollup-plugin-eslint

eslint位置很重要引瀑,放在babel插件后面會導致定位問題的時候出錯

// rollup.config.js
...
import { eslint } from 'rollup-plugin-eslint';

module.exports = {
    ...,
    plugins: [
        ...,
        eslint({ // eslint插件必須放在babel插件之前狂芋,不然檢測的是轉(zhuǎn)換后的文件,導致檢測有誤
            throwOnError: true,
            throwOnWarning: true,
            include: ['src/**'],
            exclude: ['node_modules/**']
        }),
        ...
    ]
}

根目錄下新增文件.eslitrc.js

module.exports = {
    //一旦配置了root憨栽,ESlint停止在父級目錄中查找配置文件
    root: true,
    parser: "babel-eslint", // 配置babel-eslint帜矾,避免在使用es6類屬性時,eslint報Parsing error: Unexpected token
    //想要支持的JS語言選項
    parserOptions: {
        //啟用ES6語法支持(如果支持es6的全局變量{env: {es6: true}}徒像,則默認啟用ES6語法支持)
        //此處也可以使用年份命名的版本號:2015
        ecmaVersion: 6,
        //默認為script
        sourceType: "module",
        //支持其他的語言特性
        ecmaFeatures: {}
    },
    //代碼運行的環(huán)境黍特,每個環(huán)境都會有一套預定義的全局對象,不同環(huán)境可以組合使用
    env: {
        amd: true, // 否則會出現(xiàn)'require' is not defined 提示
        es6: true,
        browser: true,
        jquery: true
    },
    //訪問當前源文件中未定義的變量時锯蛀,no-undef會報警告。
    //如果這些全局變量是合規(guī)的次慢,可以在globals中配置旁涤,避免這些全局變量發(fā)出警告
    globals: {
        //配置給全局變量的布爾值,是用來控制該全局變量是否允許被重寫
        test_param: true,
        window: true,
        process: false,
    },
    //集成推薦的規(guī)則
    extends: ["eslint:recommended"],
    //啟用額外的規(guī)則或者覆蓋默認的規(guī)則
    //規(guī)則級別分別:為"off"(0)關(guān)閉迫像、"warn"(1)警告劈愚、"error"(2)錯誤--error觸發(fā)時,程序退出
    rules: {
        //關(guān)閉“禁用console”規(guī)則
        "no-console": "off",
        //縮進不規(guī)范警告闻妓,要求縮進為2個空格菌羽,默認值為4個空格
        "indent": ["warn", 4, {
            //設置為1時強制switch語句中case的縮進為2個空格
            "SwitchCase": 1,
        }],
        // 函數(shù)定義時括號前面要不要有空格
        "space-before-function-paren": [0, "always"],
        //定義字符串不規(guī)范錯誤,要求字符串使用雙引號
        // quotes: ["error", "double"],
        //....
        //更多規(guī)則可查看http://eslint.cn/docs/rules/
    }
}
(5)一次編譯由缆,同時打包生成不同格式文件注祖,如cjs、es均唉、umd等

有兩種方法:

首先是晨,npm i -D rollup-plugin-serve rollup-plugin-uglify

修改packag.json

{
    ...,
    "module": "es/index.js",
    "main": "lib/index.js",
    "scripts": {
        "build": "rollup -c ./build/rollup.config.js"
        "clean": "rm -rf ./dist/ ./es/ ./lib/",
        "easy": "npm run clean && NODE_ENV=development rollup -w -c ./build/easy.config.js",
        "node:dev": "npm run clean && NODE_ENV=development node ./build/dev.js",
        "node:build": "npm run clean && NODE_ENV=production node ./build/build.js",
        "start": "npm run clean && NODE_ENV=development rollup -w -c ./build/rollup.config.js",
        "build": "npm run clean && NODE_ENV=production rollup -c ./build/rollup.config.js"
   },
   "files": [
        "dist",
        "lib",
        "es",
        "types"
  ],

注意:mac可以直接使用NODE_ENV=development方式傳遞變量,window下不一定可以舔箭,如果失敗請引入cross-env

(1)第一種方法:使用rollup命令打包

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babelPlugin from 'rollup-plugin-babel';
import serve from 'rollup-plugin-serve';
import { uglify } from 'rollup-plugin-uglify';
import { eslint } from 'rollup-plugin-eslint'

const path = require('path');
const resolveFile = function (filePath) {
    return path.join(__dirname, '..', filePath)
}
const isDev = process.env.NODE_ENV !== 'production';
console.log('----------dev:', process.env.NODE_ENV, isDev)

// 通過控制outputs中對應的isExternal罩缴、isUglify值來決定打包的文件是否啟用external和uglify
const outputs = [
    {
        file: resolveFile('lib/index.js'),
        format: 'cjs',
        isExternal: true,
    },
    {
        file: resolveFile('es/index.js'),
        format: 'es',
        isExternal: true,
    },
    {
        file: resolveFile('dist/index.js'),
        format: 'umd',
        name: 'npmLibraryDemo',
    },
    {
        file: resolveFile('dist/index.min.js'),
        format: 'umd',
        name: 'npmLibraryDemo',
        isUglify: true,
    }
].map(i => {
    i.sourcemap = isDev; // 開發(fā)模式:開啟sourcemap文件的生成
    return i;
});
const len = outputs.length;

const config = outputs.map((output, i) => {
    const isUglify = output.isUglify || false;
    const isExternal = output.isExternal || false;
    console.log('------config:', isExternal)
    return {
        input: resolveFile('src/main.js'),
        output,
        plugins: [
            // rollup-plugin-commonjs應該用在其他插件轉(zhuǎn)換你的模塊之前 - 這是為了防止其他插件的改變破壞CommonJS的檢測
            // 作用:將CommonJS模塊轉(zhuǎn)換為 ES2015 供 Rollup 處理
            commonjs(),
            // 作用:處理json格式文件
            json(),
            // 作用:告訴 Rollup 如何查找外部模塊
            resolve({
                // 將自定義選項傳遞給解析插件
                customResolveOptions: {
                    moduleDirectory: 'node_modules'
                }
            }),
            eslint({
                throwOnError: true,
                throwOnWarning: true,
                include: ['src/**'],
                exclude: ['node_modules/**']
            }),
            babelPlugin({
                exclude: 'node_modules/**', // 只編譯我們的源代碼
                runtimeHelpers: true,
            }),
            ...(
                isDev && i === len - 1 ?
                    [
                        serve({ // 使用開發(fā)服務插件
                            port: 3001,
                            // 設置 exmaple的訪問目錄和dist的訪問目錄
                            contentBase: [resolveFile('example'), resolveFile('dist')]
                        })
                    ] : isUglify ? [
                        uglify()
                    ] : []
            )
        ],
        // 作用:指出應將哪些模塊視為外部模塊,否則會被打包進最終的代碼里
        external: id => {
            return !isExternal ? false :
                (/@babel\/runtime/.test(id) || /lodash/.test(id));
        }
    }
})

export default config;

使用npm start開啟開發(fā)模式;使用npm run build可以打包出文件箫章;總共導出三種格式文件:cjs烙荷、es、umd檬寂,umd格式的文件有壓縮和未壓縮

start時如果報錯“getaddrinfo ENOTFOUND localhost”奢讨,參考這篇方法解決

(2)第二種方法:使用rollup api進行打包

build文件夾下新增文件:node.config.js、dev.js焰薄、build.js

// node.config.js
const json = require('rollup-plugin-json');
const resolve = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');
const babelPlugin = require('rollup-plugin-babel');
const { uglify } = require('rollup-plugin-uglify');
const path = require('path');
const isDev = process.env.NODE_ENV !== 'production';

const resolveFile = function (filePath) {
    return path.join(__dirname, '..', filePath)
}

module.exports.outputs = [
    {
        file: resolveFile('lib/index.js'),
        format: 'cjs',
        isExternal: true,
    },
    {
        file: resolveFile('es/index.js'),
        format: 'es',
        isExternal: true,
    },
    {
        file: resolveFile('dist/index.js'),
        format: 'umd',
        name: 'npmLibraryDemo',
    },
    {
        file: resolveFile('dist/index.min.js'),
        format: 'umd',
        name: 'npmLibraryDemo',
        isUglify: true,
    }
].map(i => {
    i.sourcemap = isDev; // 開發(fā)模式:開啟sourcemap文件的生成
    return i;
});

module.exports.configFun = function config({isUglify, isExternal} = {}) {
    return {
        input: resolveFile('src/main.js'),
        plugins: [
            // rollup-plugin-commonjs應該用在其他插件轉(zhuǎn)換你的模塊之前 - 這是為了防止其他插件的改變破壞CommonJS的檢測
            // 作用:將CommonJS模塊轉(zhuǎn)換為 ES2015 供 Rollup 處理
            commonjs(),
            // 作用:處理json格式文件
            json(),
            // 作用:告訴 Rollup 如何查找外部模塊
            resolve({
                // 將自定義選項傳遞給解析插件
                customResolveOptions: {
                    moduleDirectory: 'node_modules'
                }
            }),
            babelPlugin({
                exclude: 'node_modules/**', // 只編譯我們的源代碼
                runtimeHelpers: true,
            }),
            ...(
                isUglify ? [ uglify() ] : []
            )
        ],
        // 作用:指出應將哪些模塊視為外部模塊拿诸,否則會被打包進最終的代碼里
        external: id => {
            return !isExternal ? false :
                (/@babel\/runtime/.test(id) || /lodash/.test(id));
        },
    }
};
// dev.js
const path = require('path');
const serve = require('rollup-plugin-serve');
const rollup = require('rollup');
const { configFun, outputs } = require('./node.config.js');

const resolveFile = function (filePath) {
    return path.join(__dirname, '..', filePath)
}

let watchOptions = [];
const len = outputs.length;
outputs.forEach((output, i) => {
    let options = {
        isUglify: output.isUglify,
        isExternal: output.isExternal,
    }
    let config = {
        output,
        ...configFun(options)
    };
    if (i === len - 1) {
        config.plugins.push(
            serve({ // 使用開發(fā)服務插件
                port: 3001,
                // 設置 exmaple的訪問目錄和dist的訪問目錄
                contentBase: [resolveFile('example'), resolveFile('dist')]
            })
        );
    }
    watchOptions.push(config);
});

const watcher = rollup.watch(watchOptions);

watcher.on('event', event => {
    // event.code 會是下面其中一個:
    //   START        — 監(jiān)聽器正在啟動(重啟)
    //   BUNDLE_START — 構(gòu)建單個文件束
    //   BUNDLE_END   — 完成文件束構(gòu)建
    //   END          — 完成所有文件束構(gòu)建
    //   ERROR        — 構(gòu)建時遇到錯誤
    //   FATAL        — 遇到無可修復的錯誤
    switch (event.code) {
        case 'START':
            console.log(`[info] 監(jiān)聽器正在啟動(重啟)`);
            break;
        case 'BUNDLE_START':
            console.log(`[info] 開始構(gòu)建 ${event.output}`);
            break;
        case 'BUNDLE_END':
            console.log(`[info] 完成構(gòu)建 ${event.output}`);
            console.log(`[info] 構(gòu)建時長 ${event.duration}`);
            break;
        case 'END':
            console.log(`[info] 完成所有構(gòu)建`);
            break;
        case 'ERROR':
        case 'FATAL':
            console.log(`[error] 構(gòu)建發(fā)生錯誤`);
    }
});

// 停止監(jiān)聽
// watcher.close();
// build.js
const rollup = require('rollup');
const { configFun, outputs } = require('./node.config.js');

outputs.forEach(async (output) => {
    const inputOptions = configFun({
        isUglify: output.isUglify,
        isExternal: output.isExternal,
    });
    build(inputOptions, output);
})

async function build(inputOptions, outputOptions) {
    console.log(`[INFO] 開始編譯 ${inputOptions.input}`);
    // create a bundle
    const bundle = await rollup.rollup(inputOptions);

    // generate code and a sourcemap
    const res = await bundle.generate(outputOptions);
    console.log(`[INFO] ${res}`);

    // or write the bundle to disk
    await bundle.write(outputOptions);
    console.log(`[SUCCESS] 編譯結(jié)束 ${outputOptions.file}`);
}

使用npm run node:dev開啟開發(fā)模式;使用npm run node:build可以打包出文件塞茅;

可以實現(xiàn)一個簡單功能實驗一下配置是否成功亩码,比如這次提交簡單預加載圖片

注意:

1、examp/index.html中引入的js是umd形式的野瘦,如果我們的代碼中引入了運行時需要使用到的第三方包(例如lodash等)忠烛,并且沒有在index.html手動將該包引入钞脂,會導致找不到該包而報錯;因此我這里的配置中,輸出文件如果是umd格式的儒旬,就不配置external,直接將第三方包的代碼一起打包進最終的打包文件中展懈;

2直砂、當輸出文件是cjs或者es時,配置external汹买,即不將某些第三方包打包佩伤,減小最終的打包文件體積;由于我們把第三方包安裝在“dependencies”中晦毙,當別人加載我們的這個包時生巡,他們的項目會自動安裝我們的“dependencies”中所有的包,所以可以加載到我們開發(fā)的包中涉及到的第三方包见妒;

3孤荣、關(guān)于調(diào)試

我開發(fā)包的過程中用到了兩種調(diào)試方式:

方法1:直接通過npm start啟動時的http://localhost:3001來調(diào)試;由于開啟了rollup的監(jiān)聽功能须揣,因此當我們修改代碼時盐股,會自動構(gòu)建打包出新代碼,只要刷新瀏覽器就能看到最新的效果返敬;

開啟source map調(diào)試我只在方法1的調(diào)試方法中能正常使用

方法2:在項目中調(diào)試正在開發(fā)的包:

npm link命令通過鏈接目錄和可執(zhí)行文件遂庄,實現(xiàn)任意位置的npm包命令的全局可執(zhí)行。

在包目錄下執(zhí)行npm link(假設包名為pky-test)劲赠;

在項目目錄下執(zhí)行npm link pky-test即可使用該包(執(zhí)行npm unlink pky-test可以刪除包鏈接)涛目;

在包目錄下執(zhí)行npm start可以實時打包出最新代碼

6秸谢、發(fā)布,增加命令實現(xiàn)自動打標簽并根據(jù)提交記錄生成changelog

npm i -D conventional-changelog-cli

package.json

{
    ...,
    "scripts": {
        ...,
        "tag": "node ./build/version.js",
        "x": "npm --no-git-tag-version version major",
        "y": "npm --no-git-tag-version version minor",
        "z": "npm --no-git-tag-version version patch",
        "postversion": "npm run changelog && git add . && npm run tag",
        "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
        "prepublishOnly": "npm run build",
        "postpublish": "npm run clean"
    }
}

新建文件build/version.js:根據(jù)packag.json中的version提交代碼并且打標簽

const fs = require('fs');
const path = require('path');
const pathname = path.resolve(__dirname, '../package.json');
const pkg = JSON.parse(fs.readFileSync(pathname, 'utf-8'));
let version = pkg.version;

console.log('version:', version)

const exec = require('child_process').exec;
let cmdStr = `git commit -m "v${version}" && git push && git tag -a "v${version}" -m "${version}" && git push origin --tags`;
exec(cmdStr, function (err, stdout, stderr) {
    console.log('exec:', err, stdout, stderr);
});

(1)執(zhí)行npm run x/y/z可以改變package.json中的version霹肝,然后根據(jù)提交的commit信息自動生成changelog估蹄,最后會根據(jù)version提交代碼并打標簽;

(2)執(zhí)行npm run publish發(fā)布代碼

其他:

(1)我沒有使用preversion鉤子和
conventional-changelog-cli自動生成changelog沫换,因為如果在改變版本號之前執(zhí)行自動生成changelog臭蚁,那么當前版本提交的commit信息不會被自動生成到changelog中(因為changelog只會生成當前版本之前的commit記錄)

(2)必須遵循一定的commit規(guī)范,才能根據(jù)commit記錄自動生成changelog讯赏,具體自行百度下conventional-changelog-cli的使用哦

因此垮兑,推薦的工作流:

1.改動代碼

2.提交這些改動

3.改變package.json中的版本號

4.使用conventional-changelog工具

5.提交 package.json和CHANGELOG.md文件

6.打標簽tag

7.push代碼

可以參考使用conventional-changelog生成版本日志

三、知識點

先學習下以下兩篇文章:
如何開發(fā)和維護一個npm項目
你所需要的npm知識儲備都在這了

1漱挎、package.json中需要注意的點:

(1)version:

版本格式: [主版本號major.次版本號minor.修訂號patch]

先行版本: 內(nèi)部版本alpha系枪、公測版本beta、Release candiate正式版本的候選版本rc磕谅,例如1.0.0-alpha私爷、1.0.0-beta.1

使用npm version進行版本號管理:

npm version 1.0.1  # 顯示設置版本號為 1.0.1
npm version major  # major + 1,其余版本號歸 0
npm version minor  # minor + 1膊夹,patch 歸 0
npm version patch  # patch + 1

# 預發(fā)布版本
# 當前版本號為 1.2.3
npm version prepatch  # 版本號變?yōu)?1.2.4-0衬浑,也就是 1.2.4 版本的第一個預發(fā)布版本
npm version preminor  # 版本號變?yōu)?1.3.0-0,也就是 1.3.0 版本的第一個預發(fā)布版本
npm version premajor  # 版本號變?yōu)?2.0.0-0放刨,也就是 2.0.0 版本的第一個預發(fā)布版本
npm version prerelease  # 版本號變?yōu)?2.0.0-1工秩,也就是使預發(fā)布版本號加一

# 在git環(huán)境下npm version會默認執(zhí)行g(shù)it add->git commit->git tag
npm version minor -m "feat(version): upgrade to %s"  # 可自定義commit message;%s 會自動替換為新版本號

# 模塊 tag 管理
# 當前版本為1.0.1
npm version prerelease  # 1.0.2-0
npm publish --tag beta # 發(fā)布包beta版本宏榕,打上beta tag
npm dist-tag ls xxx  # 查看某個包的tag拓诸;beta: 1.0.2-0
npm install xxx@beta  # 下載beta版本 1.0.2-0
# 當prerelease版本已經(jīng)穩(wěn)定了,可以將prerelease版本設置為穩(wěn)定版本
npm dist-tag add xxx@1.0.2-0 latest
npm dist-tag ls xxx  # latest: 1.0.2-0

npm version 可以更新包版本麻昼,當倉庫已經(jīng)被git初始化了,那么運行npm version修改完版本號以后馋辈,還會運行g(shù)it add 抚芦、git commit和git tag的命令,其中commit的信息默認是自改完的版本號

(2)main迈螟、module叉抡、sideEffect:

  • main、module:用來指定npm包的入口文件
  • main: npm自帶答毫,一般表示符合CommonJS規(guī)范的文件入口
  • module: 符合ES模塊規(guī)范的文件入口褥民,使得代碼可進行Tree Shaking;并且在webpack的默認配置中洗搂,module的優(yōu)先級要高于main

因為一般項目配置babel時消返,為了加速項目編譯過程载弄,會忽略node_modules中的模塊,所以module入口的文件最好是符合ESmodule規(guī)范的ES5的代碼(說白了就是該文件只有導入導出是用的ES6模塊化語法撵颊,其他都已經(jīng)轉(zhuǎn)成了es5)宇攻,webpack最終會把ESmodule轉(zhuǎn)換為它自己的commonjs規(guī)范的代碼

  • sideEffect:webpack4中新增特性,表示npm包的代碼是否有副作用倡勇;

sideEffect可設置為Boolean或者數(shù)組;當為false時逞刷,表明這個包是沒有副作用的,可以進行按需引用;如果為數(shù)組時妻熊,數(shù)組的每一項表示的是有副作用的文件在組件庫開發(fā)的時候夸浅,如果有樣式文件,需要把樣式文件的路徑放到sideEffect的數(shù)組中扔役,因為UglifyJs只能識別js文件帆喇,如果不設置的話,最后打包的時候會把樣式文件忽略掉厅目。

由于webpack4引入了sideEffect番枚,因此當?shù)谌桨O置了sideEffect時,可以直接去除沒有用到的代碼损敷,比如antd組件庫設置sideEffect葫笼,那在webpack4時就不用再依賴babel-plugin-import進行按需加載了,webpack打包時直接就能把沒用到的代碼通過tree-shaking清除掉拗馒。

參考文章:
package.json 中的 Module 字段是干嘛的
聊聊 package.json 文件中的 module 字段

(3)tree shaking路星,用來剔除 JavaScript 中用不上的死代碼

更多詳情可參考使用 Tree Shaking

要讓 Tree Shaking 正常工作的前提是交給 Webpack 的 JavaScript 代碼必須是采用 ES6 模塊化語法的。 因為 ES6 模塊化語法是靜態(tài)的(導入導出語句中的路徑必須是靜態(tài)的字符串诱桂,而且不能放入其它代碼塊中)洋丐,這讓 Webpack 可以簡單的分析出哪些 export 的被 import 過了。 如果你采用 ES5 中的模塊化挥等,例如 module.export={...}友绝、 require(x+y)、 if(x){require('./util')}肝劲,Webpack 無法分析出哪些代碼可以剔除迁客。

基于以上說明,需要做一些配置讓tree shaking生效:

第一種情況--針對項目:

  • 把采用 ES6 模塊化的代碼直接交給 Webpack辞槐,需要配置 Babel 讓其保留 ES6 模塊化語句掷漱,修改 .babelrc 文件如下;要剔除用不上的代碼還得經(jīng)過 UglifyJS 去處理一遍榄檬,因此需要在項目中引入UglifyJSPlugin卜范;
{
    "presets": [
        [
            "env",
            {
                "modules": false
            }
        ]
    ]
}
  • 在package.json中根據(jù)實際情況設置sideEffects,詳細解釋請看上面的第(2)點

第二種情況--針對npm包開發(fā):

  • 提供兩份代碼鹿榜,一份采用 CommonJS 模塊化語法海雪,一份采用 ES6 模塊化語法锦爵,package.json 文件中有兩個字段:
{
  "main": "lib/index.js", // 指明采用 CommonJS 模塊化的代碼入口
  "module": "es/index.js" // 指明采用 ES6 模塊化的代碼入口;當該代碼存在時喳魏,webpack會優(yōu)先加載這個代碼
}
  • 根據(jù)情況設置package.json中的sideEffects字段

關(guān)于tree shaking棉浸、sideEffects使用請查看:
Tree-Shaking性能優(yōu)化實踐 - 原理篇
你的Tree-Shaking并沒什么卵用
深入淺出 sideEffects
Webpack 中的 sideEffects 到底該怎么用

由文章可知sideEffects并不是在項目真的不存在副作用代碼時才可以設置

2、控制npm發(fā)布的包包含的文件有以下方式:

  • package.json#files:數(shù)組刺彩,表示可以包含哪些文件迷郑,格式和.gitignore的寫法一樣
  • .npmignore:表示哪些文件將被忽略,格式和.gitignore的寫法一樣
  • .gitignore:表示要忽略哪些文件

優(yōu)先級:files > .npmignore > .gitignore

3创倔、package-lock.json:

  • package-lock.json把所有依賴按照順序列出來嗡害,第一次出現(xiàn)的包名會提升到頂層,后面重復出現(xiàn)的將會放入被依賴包的node_modules當中畦攘,因此會引起不完全扁平化問題霸妹。
  • 在開發(fā)應用時,建議把package-lock.json文件提交到代碼倉庫知押,從而讓團隊成員叹螟、運維部署人員或CI系統(tǒng)可以在執(zhí)行npm install時安裝的依賴版本都是一致的。
  • 但在開發(fā)一個庫時台盯,則不應把package-lock.json文件提交到倉庫中罢绽。實際上,npm也默認不會把package-lock.json文件發(fā)布出去静盅。之所以這么做良价,是因為庫項目一般是被其他項目依賴的,在不寫死的情況下蒿叠,就可以復用主項目已經(jīng)加載過的包明垢,而一旦庫依賴的是精確的版本號那么可能會造成包的冗余。

4市咽、npm scripts 腳本痊银、npx、path環(huán)境變量

package.json:

"scripts": {
    "serve": "vue-cli-service serve",
    ...
}

原理: package.json 中的 bin 字段施绎;字段 bin 表示一個可執(zhí)行文件到指定文件源的映射曼验。

例如在@vue/cli-service的package.json中:

"bin": {
    "vue-cli-service": "bin/vue-cli-service.js"
}

npx:方便調(diào)用項目內(nèi)部安裝的模塊

PATH環(huán)境變量:執(zhí)行env可查看當前所有環(huán)境變量;npm run env可查看腳本運行時的環(huán)境變量;通過npm run可在不添加路徑前綴的情況下直接訪問當前項目node_modules/.bin目錄里面的可執(zhí)行文件

5粘姜、其他

(1)

npm outdated # 查看當前項目中可升級的模塊

npm audit [--json]  # 安全漏洞檢查;加上--json熔酷,以 JSON 格式生成漏洞報告

npm audit fix # 修復存在安全漏洞的依賴包(自動更新到兼容的安全版本)

npm audit fix --force # 將依賴包版本號升級到最新的大版本孤紧,而不是兼容的安全版本;盡量避免使用--force

(2)git提交可參考以下規(guī)范:

feat:新功能(feature)

fix:修補bug

docs:文檔(documentation)

style: 格式(不影響代碼運行的變動)

refactor:重構(gòu)(即不是新增功能拒秘,也不是修改bug的代碼變動)

test:增加測試

chore:構(gòu)建過程或輔助工具的變動

(3)npm包發(fā)布流程:

于Webpack和ES6構(gòu)建NPM包

從dist到es:發(fā)一個NPM庫号显,我蛻了一層皮

8102年底如何開發(fā)和維護一個npm項目

(4)幾點心得:

1臭猜、對于webpack構(gòu)建的項目或者包,在babel中設置"modules": false其實只是讓項目中經(jīng)過babel轉(zhuǎn)化后的代碼(已經(jīng)是es5)仍然保留 ES6 模塊化語句押蚤,也就是只有導入導出語句保留es6寫法蔑歌;此時webpack會自動再去轉(zhuǎn)換這里的es6模塊化語句;也就是ES6 模塊化語句交給webpack自己去轉(zhuǎn)換揽碘;

2次屠、對于webpack構(gòu)建生成的包,不支持導出為es6模塊(最終都轉(zhuǎn)成了es5雳刺,無法保留ES6 模塊化語句不轉(zhuǎn)換)劫灶,因此如果開發(fā)的npm包希望導出多種格式,推薦使用rollup

3掖桦、為了加速項目編譯過程本昏,一般都會設置忽略編譯node_modules中的模塊,所以這就需要我們開發(fā)的npm包是編譯過的枪汪;

一般來說涌穆,用于node環(huán)境的包,只要提供符合CMD規(guī)范的包雀久,但用于web的包宿稀,就要提供更多的選項:

  • lib:符合commonjs規(guī)范的文件,一般放在lib這個文件夾里面岸啡,入口是mian
  • es:符合ES module規(guī)范的文件原叮,一般放在es這個文件夾里面,入口是module
  • dist:經(jīng)過壓縮的文件巡蘸,一般是可以通過script標簽直接引用的文件
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奋隶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悦荒,更是在濱河造成了極大的恐慌唯欣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搬味,死亡現(xiàn)場離奇詭異境氢,居然都是意外死亡,警方通過查閱死者的電腦和手機碰纬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門萍聊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悦析,你說我怎么就攤上這事寿桨。” “怎么了强戴?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵亭螟,是天一觀的道長挡鞍。 經(jīng)常有香客問我,道長预烙,這世上最難降的妖魔是什么墨微? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮扁掸,結(jié)果婚禮上翘县,老公的妹妹穿的比我還像新娘。我一直安慰自己也糊,他們只是感情好炼蹦,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狸剃,像睡著了一般掐隐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钞馁,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天虑省,我揣著相機與錄音,去河邊找鬼僧凰。 笑死探颈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的训措。 我是一名探鬼主播伪节,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绩鸣!你這毒婦竟也來了怀大?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤呀闻,失蹤者是張志新(化名)和其女友劉穎化借,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捡多,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡蓖康,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了垒手。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒜焊。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖科贬,靈堂內(nèi)的尸體忽然破棺而出山涡,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布鸭丛,位于F島的核電站,受9級特大地震影響唐责,放射性物質(zhì)發(fā)生泄漏鳞溉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一鼠哥、第九天 我趴在偏房一處隱蔽的房頂上張望熟菲。 院中可真熱鬧,春花似錦朴恳、人聲如沸抄罕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呆贿。三九已至,卻和暖如春森渐,著一層夾襖步出監(jiān)牢的瞬間做入,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工同衣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留竟块,地道東北人。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓耐齐,卻偏偏與公主長得像浪秘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子埠况,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354