1.0.4 • Published 5 years ago

npm-library-demo v1.0.4

Weekly downloads
2
License
ISC
Repository
github
Last release
5 years ago

rollup开发依赖包(npm library)实战

本文涉及包版本:node 11.6.0 、npm 6.11.3、webpack 4.39.3;使用mac开发;

项目

源码 -> https://github.com/jiaoyanlin/npm-library-demo ,求star😄

npm i
npm start

建议开始动手实践前先浏览下本文的的 三、知识点

一、发布包基本流程

1、使用nrm管理npm源:

nrm:npm registry 管理工具,方便切换不同的源;我们开发的包要发布的源是https://registry.npmjs.org,更详细的安装可以参考[nrm —— 快速切换 NPM 源](https://segmentfault.com/a/1190000000473869)

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

2、发布包:

记得先在 https://www.npmjs.com 注册账户并在邮箱激活账户

(1)编写包代码(npm init等操作,具体在下面会提及)

(2)切换registry到npm对应链接https://registry.npmjs.org/:nrm use npm

(3)登录:npm login

(4)发布、更新:npm publish

3、关于为何选择rollup而不是webpack编写一个npm包

为了支持tree shaking,得导出一份符合es6模块规范的代码,但是webpack不支持导出为es6模块,所以使用rollup来开发我们的包

rollup和webpack使用场景分析中提到:Rollup偏向应用于js库,webpack偏向应用于前端工程,UI库;如果你的应用场景中只是js代码,希望做ES转换,模块解析,可以使用Rollup。如果你的场景中涉及到css、html,涉及到复杂的代码拆分合并,建议使用webpack。

rollup可以直接构建出符合es6模块规范的代码(有利于tree shaking),但是webpack不能;因此为了更好地使用es6模块化来实现tree shaking,以及优化包代码体积等原因,选用rollup来开发npm包;

二、使用rollup构建npm包

以下内容引自rollup中文网: 为了确保你的 ES6 模块可以直接与「运行在 CommonJS(例如 Node.js 和 webpack)中的工具(tool)」使用,你可以使用 Rollup 编译为 UMD 或 CommonJS 格式,然后在 package.json 文件的 main 属性中指向当前编译的版本。如果你的 package.json 也具有 module 字段,像 Rollup 和 webpack 2 这样的 ES6 感知工具(ES6-aware tools)将会直接导入 ES6 模块版本。

关于rollup更加详细的介绍及使用,可以参考以下文章:Rollup:下一代ES模块打包工具rollup中文网Rollup.js 实战学习笔记webpack创建library

1、先来个简单的demo:源码

(1)新建一个文件夹npm-library-demo

初始化:

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

根据以下目录结构新增文件夹及文件:

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

(2)文件内容:

package.json中加入构建脚本命令:

"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)执行npm run build,就可以生成打包文件/dist/bundle.js,打开example/index.html控制台可以查看打包文件是否生效

2、使用插件,在1的基础上进行以下操作:源码

在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中的数据
console.log('version ' + version);
export default (function () {
	console.log(foo);
})();

此时再次使用npm run build打包,打开index.html,在控制台可以看到相关结果

其他插件使用方式类似

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模块转换为 ES2015 供 Rollup 处理,请注意,rollup-plugin-commonjs应该用在其他插件转换你的模块之前 - 这是为了防止其他插件的改变破坏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 接受一个模块名称的数组或一个接受模块名称的函数,如果它被视为外部引用(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,可以直接在控制台执行node ./dist/bundle.js测试打包结果;此时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 规范
                // "targets": "> 0.25%, last 2 versions, iOS >= 8, Android >= 4.4, not dead"
            }
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                // useESModules:引入的helpers是否是es modules规范的;注意当打包成cjs时不能引入es modules下的代码,会报错
                // "useESModules": true,
                "corejs": 2 // 参考官方文档
            }
        ],
    ]
}

可以自己在main.js中加入一些es6语法,看看打包后的文件是否将es6语法编译成了es5(如const、let等)

??????? babel还有一篇相关博文补充

(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插件之前,不然检测的是转换后的文件,导致检测有误
            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: {}
    },
    //代码运行的环境,每个环境都会有一套预定义的全局对象,不同环境可以组合使用
    env: {
        amd: true, // 否则会出现'require' is not defined 提示
        es6: true,
        browser: true,
        jquery: true
    },
    //访问当前源文件中未定义的变量时,no-undef会报警告。
    //如果这些全局变量是合规的,可以在globals中配置,避免这些全局变量发出警告
    globals: {
        //配置给全局变量的布尔值,是用来控制该全局变量是否允许被重写
        test_param: true,
        window: true,
        process: false,
    },
    //集成推荐的规则
    extends: ["eslint:recommended"],
    //启用额外的规则或者覆盖默认的规则
    //规则级别分别:为"off"(0)关闭、"warn"(1)警告、"error"(2)错误--error触发时,程序退出
    rules: {
        //关闭“禁用console”规则
        "no-console": "off",
        //缩进不规范警告,要求缩进为2个空格,默认值为4个空格
        "indent": ["warn", 4, {
            //设置为1时强制switch语句中case的缩进为2个空格
            "SwitchCase": 1,
        }],
        // 函数定义时括号前面要不要有空格
        "space-before-function-paren": [0, "always"],
        //定义字符串不规范错误,要求字符串使用双引号
        // quotes: ["error", "double"],
        //....
        //更多规则可查看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; // 开发模式:开启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应该用在其他插件转换你的模块之前 - 这是为了防止其他插件的改变破坏CommonJS的检测
            // 作用:将CommonJS模块转换为 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({ // 使用开发服务插件
                            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开启开发模式;使用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; // 开发模式:开启sourcemap文件的生成
    return i;
});

module.exports.configFun = function config({isUglify, isExternal} = {}) {
    return {
        input: resolveFile('src/main.js'),
        plugins: [
            // rollup-plugin-commonjs应该用在其他插件转换你的模块之前 - 这是为了防止其他插件的改变破坏CommonJS的检测
            // 作用:将CommonJS模块转换为 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({ // 使用开发服务插件
                port: 3001,
                // 设置 exmaple的访问目录和dist的访问目录
                contentBase: [resolveFile('example'), resolveFile('dist')]
            })
        );
    }
    watchOptions.push(config);
});

const watcher = rollup.watch(watchOptions);

watcher.on('event', event => {
    // event.code 会是下面其中一个:
    //   START        — 监听器正在启动(重启)
    //   BUNDLE_START — 构建单个文件束
    //   BUNDLE_END   — 完成文件束构建
    //   END          — 完成所有文件束构建
    //   ERROR        — 构建时遇到错误
    //   FATAL        — 遇到无可修复的错误
    switch (event.code) {
        case 'START':
            console.log(`[info] 监听器正在启动(重启)`);
            break;
        case 'BUNDLE_START':
            console.log(`[info] 开始构建 ${event.output}`);
            break;
        case 'BUNDLE_END':
            console.log(`[info] 完成构建 ${event.output}`);
            console.log(`[info] 构建时长 ${event.duration}`);
            break;
        case 'END':
            console.log(`[info] 完成所有构建`);
            break;
        case 'ERROR':
        case 'FATAL':
            console.log(`[error] 构建发生错误`);
    }
});

// 停止监听
// 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] 编译结束 ${outputOptions.file}`);
}

使用npm run node:dev开启开发模式;使用npm run node:build可以打包出文件;

可以实现一个简单功能实验一下配置是否成功,比如这次提交简单预加载图片

注意:

1、examp/index.html中引入的js是umd形式的,如果我们的代码中引入了运行时需要使用到的第三方包(例如lodash等),并且没有在index.html手动将该包引入,会导致找不到该包而报错;因此我这里的配置中,输出文件如果是umd格式的,就不配置external,直接将第三方包的代码一起打包进最终的打包文件中;

2、当输出文件是cjs或者es时,配置external,即不将某些第三方包打包,减小最终的打包文件体积;由于我们把第三方包安装在“dependencies”中,当别人加载我们的这个包时,他们的项目会自动安装我们的“dependencies”中所有的包,所以可以加载到我们开发的包中涉及到的第三方包;

3、关于调试

我开发包的过程中用到了两种调试方式:

方法1:直接通过npm start启动时的http://localhost:3001来调试;由于开启了rollup的监听功能,因此当我们修改代码时,会自动构建打包出新代码,只要刷新浏览器就能看到最新的效果;

开启source map调试我只在方法1的调试方法中能正常使用

方法2:在项目中调试正在开发的包:

npm link命令通过链接目录和可执行文件,实现任意位置的npm包命令的全局可执行。

在包目录下执行npm link(假设包名为pky-test);

在项目目录下执行npm link pky-test即可使用该包(执行npm unlink pky-test可以删除包链接);

在包目录下执行npm start可以实时打包出最新代码

6、发布,增加命令实现自动打标签并根据提交记录生成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:根据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)执行npm run x/y/z可以改变package.json中的version,然后根据提交的commit信息自动生成changelog,最后会根据version提交代码并打标签;

(2)执行npm run publish发布代码

其他:

(1)我没有使用preversion钩子和 conventional-changelog-cli自动生成changelog,因为如果在改变版本号之前执行自动生成changelog,那么当前版本提交的commit信息不会被自动生成到changelog中(因为changelog只会生成当前版本之前的commit记录)

(2)必须遵循一定的commit规范,才能根据commit记录自动生成changelog,具体自行百度下conventional-changelog-cli的使用哦

因此,推荐的工作流:

1.改动代码

2.提交这些改动

3.改变package.json中的版本号

4.使用conventional-changelog工具

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

6.打标签tag

7.push代码

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

三、知识点

先学习下以下两篇文章: 如何开发和维护一个npm项目 你所需要的npm知识储备都在这了

1、package.json中需要注意的点:

(1)version:

版本格式: 主版本号major.次版本号minor.修订号patch

先行版本: 内部版本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

# 预发布版本
# 当前版本号为 1.2.3
npm version prepatch  # 版本号变为 1.2.4-0,也就是 1.2.4 版本的第一个预发布版本
npm version preminor  # 版本号变为 1.3.0-0,也就是 1.3.0 版本的第一个预发布版本
npm version premajor  # 版本号变为 2.0.0-0,也就是 2.0.0 版本的第一个预发布版本
npm version prerelease  # 版本号变为 2.0.0-1,也就是使预发布版本号加一

# 在git环境下npm version会默认执行git 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 # 发布包beta版本,打上beta tag
npm dist-tag ls xxx  # 查看某个包的tag;beta: 1.0.2-0
npm install xxx@beta  # 下载beta版本 1.0.2-0
# 当prerelease版本已经稳定了,可以将prerelease版本设置为稳定版本
npm dist-tag add xxx@1.0.2-0 latest
npm dist-tag ls xxx  # latest: 1.0.2-0

npm version 可以更新包版本,当仓库已经被git初始化了,那么运行npm version修改完版本号以后,还会运行git add 、git commit和git tag的命令,其中commit的信息默认是自改完的版本号

(2)main、module、sideEffect:

  • main、module:用来指定npm包的入口文件
  • main: npm自带,一般表示符合CommonJS规范的文件入口
  • module: 符合ES模块规范的文件入口,使得代码可进行Tree Shaking;并且在webpack的默认配置中,module的优先级要高于main

因为一般项目配置babel时,为了加速项目编译过程,会忽略node_modules中的模块,所以module入口的文件最好是符合ESmodule规范的ES5的代码(说白了就是该文件只有导入导出是用的ES6模块化语法,其他都已经转成了es5),webpack最终会把ESmodule转换为它自己的commonjs规范的代码

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

sideEffect可设置为Boolean或者数组;当为false时,表明这个包是没有副作用的,可以进行按需引用;如果为数组时,数组的每一项表示的是有副作用的文件在组件库开发的时候,如果有样式文件,需要把样式文件的路径放到sideEffect的数组中,因为UglifyJs只能识别js文件,如果不设置的话,最后打包的时候会把样式文件忽略掉。

由于webpack4引入了sideEffect,因此当第三方包设置了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 模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让 Webpack 可以简单的分析出哪些 export 的被 import 过了。 如果你采用 ES5 中的模块化,例如 module.export={...}、 require(x+y)、 if(x){require('./util')},Webpack 无法分析出哪些代码可以剔除。

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

第一种情况--针对项目:

  • 把采用 ES6 模块化的代码直接交给 Webpack,需要配置 Babel 让其保留 ES6 模块化语句,修改 .babelrc 文件如下;要剔除用不上的代码还得经过 UglifyJS 去处理一遍,因此需要在项目中引入UglifyJSPlugin;
{
    "presets": [
        [
            "env",
            {
                "modules": false
            }
        ]
    ]
}
  • 在package.json中根据实际情况设置sideEffects,详细解释请看上面的第(2)点

第二种情况--针对npm包开发:

  • 提供两份代码,一份采用 CommonJS 模块化语法,一份采用 ES6 模块化语法,package.json 文件中有两个字段:
{
  "main": "lib/index.js", // 指明采用 CommonJS 模块化的代码入口
  "module": "es/index.js" // 指明采用 ES6 模块化的代码入口;当该代码存在时,webpack会优先加载这个代码
}
  • 根据情况设置package.json中的sideEffects字段

关于tree shaking、sideEffects使用请查看: Tree-Shaking性能优化实践 - 原理篇 你的Tree-Shaking并没什么卵用 深入浅出 sideEffects Webpack 中的 sideEffects 到底该怎么用

由文章可知sideEffects并不是在项目真的不存在副作用代码时才可以设置

2、控制npm发布的包包含的文件有以下方式:

  • package.json#files:数组,表示可以包含哪些文件,格式和.gitignore的写法一样
  • .npmignore:表示哪些文件将被忽略,格式和.gitignore的写法一样
  • .gitignore:表示要忽略哪些文件

优先级:files > .npmignore > .gitignore

3、package-lock.json:

  • package-lock.json把所有依赖按照顺序列出来,第一次出现的包名会提升到顶层,后面重复出现的将会放入被依赖包的node_modules当中,因此会引起不完全扁平化问题。
  • 在开发应用时,建议把package-lock.json文件提交到代码仓库,从而让团队成员、运维部署人员或CI系统可以在执行npm install时安装的依赖版本都是一致的。
  • 但在开发一个库时,则不应把package-lock.json文件提交到仓库中。实际上,npm也默认不会把package-lock.json文件发布出去。之所以这么做,是因为库项目一般是被其他项目依赖的,在不写死的情况下,就可以复用主项目已经加载过的包,而一旦库依赖的是精确的版本号那么可能会造成包的冗余。

4、npm scripts 脚本、npx、path环境变量

package.json:

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

原理: package.json 中的 bin 字段;字段 bin 表示一个可执行文件到指定文件源的映射。

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

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

npx:方便调用项目内部安装的模块

PATH环境变量:执行env可查看当前所有环境变量;npm run env可查看脚本运行时的环境变量;通过npm run可在不添加路径前缀的情况下直接访问当前项目node_modules/.bin目录里面的可执行文件

5、其他

(1)

npm outdated # 查看当前项目中可升级的模块

npm audit [--json]  # 安全漏洞检查;加上--json,以 JSON 格式生成漏洞报告

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

npm audit fix --force # 将依赖包版本号升级到最新的大版本,而不是兼容的安全版本;尽量避免使用--force

(2)git提交可参考以下规范:

feat:新功能(feature)

fix:修补bug

docs:文档(documentation)

style: 格式(不影响代码运行的变动)

refactor:重构(即不是新增功能,也不是修改bug的代码变动)

test:增加测试

chore:构建过程或辅助工具的变动

(3)npm包发布流程:

于Webpack和ES6构建NPM包

从dist到es:发一个NPM库,我蜕了一层皮

8102年底如何开发和维护一个npm项目

(4)几点心得:

1、对于webpack构建的项目或者包,在babel中设置"modules": false其实只是让项目中经过babel转化后的代码(已经是es5)仍然保留 ES6 模块化语句,也就是只有导入导出语句保留es6写法;此时webpack会自动再去转换这里的es6模块化语句;也就是ES6 模块化语句交给webpack自己去转换;

2、对于webpack构建生成的包,不支持导出为es6模块(最终都转成了es5,无法保留ES6 模块化语句不转换),因此如果开发的npm包希望导出多种格式,推荐使用rollup

3、为了加速项目编译过程,一般都会设置忽略编译node_modules中的模块,所以这就需要我们开发的npm包是编译过的;

一般来说,用于node环境的包,只要提供符合CMD规范的包,但用于web的包,就要提供更多的选项:

  • lib:符合commonjs规范的文件,一般放在lib这个文件夹里面,入口是mian
  • es:符合ES module规范的文件,一般放在es这个文件夹里面,入口是module
  • dist:经过压缩的文件,一般是可以通过script标签直接引用的文件