重新认识 package.json

前言 🤔

  • 在每个项目的根目录下面,一般都会有一个 package.json文件,其定义了运行项目所需要的各种依赖和项目的配置信息(如名称、版本、许可证等元数据)。
  • 大多数人对 package.json文件的了解,仅停留在:
    • 项目名称、项目构建版本、许可证的定义;
    • 依赖定义(包括 dependencies字段,devDependencies字段);
    • 使用scripts字段指定运行脚本命令的 npm命令行缩写。
  • 其实,package.json的作用远不止于此,我们可以通过新增配置项实现更强大的功能,下面将带你重新认识 package.json

由简入繁,丰富项目的 package.json

简单版的 package.json

  • 当我们新建一个名称为 my-test的项目时,使用 yarn init -ynpm init -y命令后,在项目目录下会新增一个 package.json文件,内容如下:
{
  "name": "my-test", # 项目名称
  "version": "1.0.0", # 项目版本(格式:大版本.次要版本.小版本)
  "description": "", # 项目描述
  "main": "index.js", # 入口文件
  "scripts": { # 指定运行脚本命令的 npm 命令行缩写
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [], # 关键词
  "author": "", # 作者
  "license": "ISC" # 许可证
}
  • 可以看到,package.json文件的内容是一个 JSON对象,对象的每一个成员就是当前项目的一项配置。

安装项目依赖(dependencies & devDependencies)

  • dependencies字段指定了项目运行所依赖的模块(生产环境使用),如 antdreactmoment等插件库:
    • 它们是我们生产环境所使用的,所以要放在 dependencies中,如果未将项目必须依赖的插件库安装到 dependencies,打包后的项目就可能报错,无法运行。
  • devDependencies字段指定了项目开发所需要的模块(开发环境使用),如 webpacktypescriptbabel等:
    • 在代码打包提交线上时,我们并不需要这些工具,所以我们将它放入 devDependencies中。
  • 如果一个模块不在 package.json文件之中,我们可以单独安装这个模块,并使用相应的参数,将其写入 dependencies字段/ devDependencies字段中:
# 使用 npm
npm install <package...> --save # 写入 dependencies 属性
npm install <package...> --save-dev # 写入 devDependencies 属性

# 使用 yarn
yarn add <package...> # 写入 dependencies 属性
yarn add <package...> --dev # 写入 devDependencies 属性
  • 有了 package.json文件,开发直接使用 npm install/ yarn install命令,就会在当前目录中自动安装所需要的模块,安装完成项目所需的运行和开发环境就配置好了。

简化终端命令(scripts)

  • scripts字段是 package.json中的一种元数据功能,它接受一个对象,对象的属性为可以通过 npm run 运行的脚本,值为实际运行的命令(通常是终端命令),如:
"scripts": {
  "start": "node index.js"
},
  • 将终端命令放入 scripts字段,既可以记录它们又可以实现轻松重用。

定义项目入口(main)

  • main字段是 package.json中的另一种元数据功能,它可以用来指定加载的入口文件。假如你的项目是一个 npm包,当用户安装你的包后,require('my-module')返回的是 main字段中所列出文件的 module.exports属性。
  • 当不指定main字段时,默认值是模块根目录下面的index.js文件。

指定项目 node 版本(engines)

  • 有时候,新拉一个项目的时候,由于和其他开发使用的 node版本不同,导致会出现很多奇奇怪怪的问题(如某些依赖安装报错、依赖安装完项目跑步起来等)。

  • 为了实现项目开箱即用的伟大理想,这时候可以使用 package.jsonengines字段来指定项目 node 版本:
"engines": {
   "node": ">= 8.16.0"
 },
  • 该字段也可以指定适用的 npm版本:
"engines": {
   "npm": ">= 6.9.0"
 },
  • 需要注意的是,engines属性仅起到一个说明的作用,当用户版本不符合指定值时也不影响依赖的安装。

自定义命令(bin)

  • 用过 vue-clicreate-react-app等脚手架的朋友们,不知道你们有没有好奇过,为什么安装这些脚手架后,就可以使用类似 vue create/create-react-app之类的命令,其实这和 package.json中的 bin字段有关。
  • bin字段用来指定各个内部命令对应的可执行文件的位置。当package.json提供了 bin字段后,即相当于做了一个命令名和本地文件名的映射。
  • 当用户安装带有 bin字段的包时,
    • 如果是全局安装,npm将会使用符号链接把这些文件链接到/usr/local/node_modules/.bin/
    • 如果是本地安装,会链接到./node_modules/.bin/
  • 举个 🌰,如果要使用 my-app-cli作为命令时,可以配置以下 bin字段:
"bin": {
  "my-app-cli": "./bin/cli.js"
}
  • 上面代码指定,my-app-cli命令对应的可执行文件为 bin子目录下的 cli.js,因此在安装了 my-app-cli包的项目中,就可以很方便地利用 npm执行脚本:
"scripts": {
  start: 'node node_modules/.bin/my-app-cli'
}

  • 咦,怎么看起来和 vue create/create-react-app之类的命令不太像?原因:
    • 当需要 node环境时就需要加上 node前缀
    • 如果加上 node前缀,就需要指定 my-app-cli的路径 -> node_modules/.bin,否则 node my-app-cli会去查找当前路径下的 my-app-cli.js,这样肯定是不对。
  • 若要实现像 vue create/create-react-app之类的命令一样简便的方式,则可以在上文提到的 bin子目录下可执行文件cli.js中的第一行写入以下命令:
#!/usr/bin/env node
  • 这行命令的作用是告诉系统用 node解析,这样命令就可以简写成 my-app-cli了。

React 项目相关

设置应用根路径(homepage)

  • 当我们使用 create-react-app脚手架搭建的 React项目,默认是使用内置的 webpack配置,当package.json中不配置 homepage属性时,build 打包之后的文件资源应用路径默认是 /,如下图:

  • 一般来说,我们打包的静态资源会部署在 CDN上,为了让我们的应用知道去哪里加载资源,则需要我们设置一个根路径,这时可以通过 package.json中的 homepage字段设置应用的根路径。
  • 当我们设置了 homepage属性后:
{
  "homepage": "https://xxxx.cdn/my-project",
}
  • 打包后的资源路径就会加上 homepage的地址:

开发环境解决跨域问题(proxy)

  • 在做前后端分离的项目的时候,调用接口时则会遇到跨域的问题,当在开发环境中时,可以通过配置 package.json中的 proxy来解决跨域问题,配置如下:
{
  "proxy": "http://localhost:4000"  // 配置你要请求的服务器地址
}
  • 注意,当 create-react-app的版本高于 2.0 版本的时候在 package.json中只能配置 string类型,这意味着如果要使用 package.json来解决跨域问题,则只能代理一个服务器地址。
  • 如果要代理多个服务器地址时,则需要安装 http-proxy-middleware,在 src目录下新建 setupProxy.js
const proxy = require("http-proxy-middleware");

module.exports = function(app) {
  app.use(
    proxy("/base", {
      target: "http://localhost:4000",
      changeOrigin: true
    })
  );
  app.use(
    proxy("/fans", {
      target: "http://localhost:5000",
      changeOrigin: true
    })
  );
};

根据开发环境采用不同的全局变量值(自定义字段)

  • 假设有这么一个组件,当组件被点击时,在开发环境时是跳转测试环境的 sentry地址,在正式环境时则跳转正式环境的 sentry地址。
  • 首先,通过配置前面提到的 scripts字段,实现环境变量(NODE_ENV)的设置:
"scripts": {
  "start": "NODE_ENV=development node scripts/start.js",
  "build": "NODE_ENV=production node scripts/build.js",
},
  • 项目启动起来后,在代码中我们可以通过 process.env.NODE_ENV访问到 NODE_ENV的值。
方案一
  • 我们可以在组件中写类似以下的判断代码,根据不同环境给 sentryUrl设置不同的值:
let sentryUrl;
if (process.env.NODE_ENV === 'development') {
    sentryUrl = 'test-sentry.xxx.com';
} else {
    sentryUrl = 'sentry.xxx.com';
}
  • 这么做好像没毛病,但是深入一想,如果有多个组件,要根据不同的环境使用不同的服务(多种服务)地址,如果按照上面的写法,项目中将存在许多重复的判断代码,且当服务地址发生变化时,包含这些服务地址的组件都需要相应的做改动,这样明显是不合理的。

方案二
  • 解决方案:相关服务的地址配置在 package.json中,同时修改项目的 webpack配置。
  • 注:修改项目的 webpack配置需要 eject 项目的 webpack配置(更多细节可阅读 👉:react + typescript 项目的定制化过程)。
  • 在项目根目录下使用 yarn eject成功 eject 出配置后,可以发现项目目录的变化如下:

  • 如果需要定制化项目,一般就是在 config目录下对默认的 webpack配置进行修改,在这里我们需要关注 config/path.jsconfig/env.js两个文件:
    • env.js的主要目的在于读取 env配置文件并将 env的配置信息给到全局变量 process.env
    • path.js的主要目的在于为项目提供各种路径,包括构建路径、 public路径等。
  • 由于本文的重点不是学习 webpack配置,这里仅介绍如何实现【根据开发环境采用不同的全局变量值】的功能。
  • 首先,需要在 package.json中配置以下内容:
"scripts": {
  "start": "NODE_ENV=development node scripts/start.js",
  "build": "NODE_ENV=production node scripts/build.js",
},
"sentryPath": {
  "dev": "https://test-sentry.xxx.com",
  "prod": "https://sentry.xxx.com"
 }
  • 然后,修改 path.js文件,内容如下:
// 重写 getPublicUrl 方法
const getPublicUrl = (appPackageJson, pathName) => {
  let path;
  switch (process.env.DEPLOY_ENV) {
    case 'development':
      path = require(appPackageJson)[pathName].dev;
      break;
    case 'production':
      path = require(appPackageJson)[pathName].prod;
      break;
    default:
      path = envPublicUrl || require(appPackageJson).homepage;
  }
  return path;
}

// 新增 getSentryPath 方法
const getSentryPath = (appPackageJson) => {
  return getPublicUrl(appPackageJson, 'sentryPath');
}

// config after eject: we're in ./config/
module.exports = {
  ...,
  sentryUrl: getSentryPath(resolveApp('package.json')), // 新增
};
  • 最后,修改 env.js文件,内容如下:
// 修改 getClientEnvironment 方法
function getClientEnvironment(publicUrl) {
  const raw = Object.keys(process.env)
    .filter(key => REACT_APP.test(key))
    .reduce(
      (env, key) => {
        ...
      },
      {
        NODE_ENV: process.env.NODE_ENV || 'development',
        PUBLIC_URL: publicUrl,
        SENTRY_URL: paths.sentryUrl // 新增
      }
    );

  const stringified = {
    ...
  };
  return { raw, stringified };
}
  • 通过上面的配置,我们就可以在组件中通过 process.env.SENTRY_URL获取到 sentry服务的地址了,虽然看起来比方案一繁琐,但是这种收益是长期的,如要新增一个 sonarqube服务,同理实现即可,通过使用 package.json也可以清楚的看到当前服务在不同环境下使用的地址。

总结 👀

  • 本文介绍了 package.json的多种常见的配置字段及作用,并通过例子加深大家对 package.json这些字段的理解。
  • 除了一些常用字段,还介绍了在React项目中 package.json文件能实现的一些功能进行介绍。

以上内容如有遗漏错误,欢迎留言 ✍️指出,一起进步💪💪💪

如果觉得本文对你有帮助,🏀🏀留下你宝贵的 👍

参考资料 📖

  1. Creating a package.json file
  2. package.json bin的作用
  3. 在开发环境中代理 API 请求
  4. react + typescript 项目的定制化过程
  5. React学习笔记
原文链接:juejin.im

上一篇:【译】11 个能让你的 Web App 像原生 App 的 Chrome API
下一篇:【译】关于 JavaScript 中 Symbol 数据类型你需要了解的一切

相关推荐

  • 🙋Hanjst汉吉斯特优化+JsonDataFromScript等

    近日继续对 🙋Hanjst汉吉斯特优化改进。这次的改进思考是从服务器端返回的 HanjstJsonData的容器设计问题。目前的做法是服务器端的HanjstJsonData放入终端页面的一个Div元...

    1 个月前
  • 高效遍历匹配Json数据,避免嵌套循环

    工作中经常会遇到这样的需求: 1.购物车列表中勾选某些,点击任意一项,前往详情页,再返回购物车依旧需要呈现勾选状态 2.勾选人员后,前往别的页面,再次返回,人员依旧程勾选状态 3.等等.......

    2 年前
  • 面试题|手写JSON解析器

    这周的 Cassidoo 的每周简讯有这么一个面试题:: 写一个函数,这个函数接收一个正确的 JSON 字符串并将其转化为一个对象(或字典,映射等,这取决于你选择的语言)。

    4 个月前
  • 隐藏某些价值观输出stringify() JSON。

    Evan CarrollNilesh(https://stackoverflow.com/users/124486/evancarroll)提出了一个问题:Hide certain values in...

    2 年前
  • 随记-package

    详情参考npmpackage.json 👈🏻 package.json 一、介绍 存在于根目录下 定义当前项目所需模块及项目配置信息 npm install 依赖当前文件 二、字段及含义 ...

    3 个月前
  • 随着Nodejs JSON对象响应(转换对象数组JSON字符串)

    Rudolf Olahclimboid(https://stackoverflow.com/users/9903/rudolfolah)提出了一个问题:Responding with a JSON o...

    2 年前
  • 铬sendrequest错误:列表的循环结构转换到JSON

    SomeKittensSkizit(https://stackoverflow.com/users/1216976/somekittens)提出了一个问题:Chrome sendrequest err...

    2 年前
  • 重新认识vue之事件阻止冒泡

    冒泡的表现 近期用vue做了一个需求,大概是同一个区域,点击不同位置有不同的响应函数,还有个总的响应函数,好吧,如下图所示: (https://img.javascriptcn.com/4ff5...

    2 年前
  • 重新认识vue之 混入 (mixins)模式

    1. 专栏(/blogs) 2. x贝贝(/blog/xiaoyan) 3. 文章详情 侯贝贝(https://img.javascriptcn.com/bb1b7...

    1 年前
  • 重新认识vue之 ref

    ref ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。此时对 ref 的认识在于,元素属性写了 refs, 则可以通过 this.$refs 访问到该元...

    2 年前

官方社区

扫码加入 JavaScript 社区