creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.64k stars 211 forks source link

开发基于Node.js的前端工具 #28

Open creeperyang opened 7 years ago

creeperyang commented 7 years ago

开发基于Node.js的前端工具

知乎上有这样一个问题:为什么node出现之后,各种前端构建工具和手段才如雨后春笋般层出不穷?,里面的答案挺有意思的。其实自从有了Node.js, Jser们可以脱离浏览器做各种各样有趣的事,在开发中,各种JS库帮助我们改善开发流程,提高开发效率, 比如webpack/babel等等。

今天这里我们就主要讲讲怎么基于Node.js来开发(小)工具,提高我们的工作效率,满足各种实际需要。

一. 相关前置知识

1. Environments

The environment is an area that the shell builds every time that it starts a session that contains variables that define system properties.

每当shell新开启一个会话时,shell都会生成environment,environment里都是些定义系统属性的变量。

$ env

TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
USER=creeper
PATH=/Users/creeper/git/depot_tools:/usr/local/sbin:/Users/creeper/.nvm/versions/node/v6.9.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/dist
...
MANPATH=/Users/creeper/.nvm/versions/node/v6.9.0/share/man:/usr/local/share/man:/usr/share/man:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/share/man:/Applications/Xcode.app/Contents/Developer/usr/share/man:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/man
NVM_PATH=/Users/creeper/.nvm/versions/node/v6.9.0/lib/node
NVM_BIN=/Users/creeper/.nvm/versions/node/v6.9.0/bin
_=/usr/bin/env

很多程序都会用到这里的变量,比如nvm会用这里的NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/dist 作为代理服务器地址,去淘宝的源下载node来安装,提高速度。

PATH

在environment这么多变量里,有一个变量需要特别注意,就是PATH=/Users/creeper/git/depot_tools:/usr/local/sbin:/Users/creeper/.nvm/versions/node/v6.9.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

In UNIX / Linux file systems, the human-readable address of a resource is defined by PATH. It is an environmental variable that tells the shell which directories to search for executable files (i.e., ready-to-run programs) in response to commands issued by a user.

PATH指定了Shell从哪些目录去查找执行文件(PATH在windows里可以通过环境变量去设置)。

当我们在shell里输入比如ls时,其实shell会找到/bin/ls来执行。

2. shebang line(#!)与可执行文件

上面一段提到了可执行文件,在这里我们只讲其中的一块——script文件。任何以shebang line(#!) 开头的文件即可执行的脚本,其中shebang line指定了用什么(解释器)来解释执行脚本。

比如常见的python脚本,你可以看到第一行是这样的:

#!/usr/bin/env python

对node.js来说,shebang line通常这么写:

#!/usr/bin/env node

对有这行的文件,当你shell里执行./my_script时,系统会调用node来解释执行my_script文件。

注意,shebang line是可以加上参数的,如#!/usr/bin/env node --harmony

以常见的webpack为例,当你npm i webpack之后,你可以找到这样一个文件node_modules/.bin/webpack, 它即webpack的可执行文件。你可以shell里执行node_modules/.bin/webpack,那么会输出help信息。

那么我们稍微看下node_modules/.bin/webpack这个文件:

#!/usr/bin/env node

/*
    MIT License http://www.opensource.org/licenses/mit-license.php
    Author Tobias Koppers @sokra
*/
var path = require("path");

果然是shebang line加上js代码。

3. 全局安装的npm包为什么可以在shell里直接使用?

接着上面两段,我们差不多明白用node.js写工具的原理了,和python没什么不同。

但是,我们全局安装的一些npm包,比如grunt/gulp/webpack,为什么可以直接在shell里执行呢,和ls之类一样?

其实就是因为:

  1. 全局安装的npm包安装的位置是固定的,可执行文件存放的位置也是固定的。比如我这里,全局包放在 /Users/creeper/.nvm/versions/node/v6.9.0/lib/node_modules,可执行文件放在/Users/creeper/.nvm/versions/node/v6.9.0/bin

    $ ls -al /Users/creeper/.nvm/versions/node/v6.9.0/bin
    drwxr-xr-x  19 creeper  staff       646  3 16 16:37 .
    drwxr-xr-x  10 creeper  staff       340 10 27 20:01 ..
    lrwxr-xr-x   1 creeper  staff        33 10 27 20:06 cnpm -> ../lib/node_modules/cnpm/bin/cnpm
    lrwxr-xr-x   1 creeper  staff        43  3 16 16:37 crn-cli -> ../lib/node_modules/@ctrip/crn-cli/index.js

当然,这里的路径都是我本机的,不同机器会有不一样的路径。另外,可执行文件用放在 这个词描述可能不准确,这里其实是 软链接(symlink

  1. /Users/creeper/.nvm/versions/node/v6.9.0/bin这个路径是在环境变量PATH里的,所以当你 执行crn-cli时,shell可以正确找到这个命令。

    PATH=/Users/creeper/git/depot_tools:/usr/local/sbin:/Users/creeper/.nvm/versions/node/v6.9.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

4. 当npm run command时,我们是在做什么?

我们一般的开发中,经常会用到npm run command,比如npm test等等,这里简单补充下。

npm run-script <command> [-- <args>...]可以执行在package.jsonscripts中的相应命令。

// 来自 React repo
{
  "scripts": {
    "build": "grunt build",
    "linc": "git diff --name-only --diff-filter=ACMRTUB `git merge-base HEAD master` | grep '\\.js$' | xargs eslint --",
    "lint": "grunt lint",
    "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
    "test": "jest",
    "flow": "flow"
  }
}

上面是从Reactpackage.json截出的,当我们在shell里执行npm run build时,其实执行的就是 grunt build

注意:除了shell已经存在的PATH,npm run会添加node_modules/.bin到PATH里。也就是说, node_modules/.bin的执行文件是可以直接执行的,不用node_modules/.bin/grunt build这种。

二. 结合实例来具体阐述工具编写

前面讲完了一些前置知识,下面结合实例来讲工具编写,主要是我们组实际用到的 :)

1. 图片处理--直接写JS

虽然前面讲了一大堆可执行文件相关的,但对一些一次性工作,我们其实可以直接写JS,然后node执行就好了。 这是最简单快捷的。

当我们shell里执行node x.js --args,args可以通过process.argv来访问:

$ node run.js *.png

[ '/Users/creeper/.nvm/versions/node/v6.9.0/bin/node',
  '/Users/creeper/Downloads/切图/run.js',
  '*.png' ]

$ node run.js test/*.jpg
[ '/Users/creeper/.nvm/versions/node/v6.9.0/bin/node',
  '/Users/creeper/Downloads/切图/run.js',
  'test/00010_西安_SIA_12_中国.jpg',
  'test/00012_南京_NKG_15_中国.jpg',
  'test/00013_无锡_WUX_15_中国.jpg',
  'test/00015_扬州_YTY_15_中国.jpg',
  'test/00017_杭州_HGH_16_中国.jpg',
  'test/00019_舟山_HSN_16_中国.jpg' ]

可以看到process.argv[0]固定是node本身路径,process.argv[1]是文件路径,process.argv.slice(2) 才是我们输入的参数。

具体到我们这里图片处理(UED有很多图片处理工作):

需求: 有一大堆大图(几百张),请导出640x420, 582×178, 284x178, 178x178, 268x106五种尺寸(中心缩放/切割), 且每种尺寸有高斯模糊和正常两种。 方案: 手动PS处理肯定不行,所以 imagemagick(负责图片处理)+ JS(负责参数和一些额外工作,比如图片分类/改名)。

核心代码:

// ...
const GaussianBlur = `20x8`
const getCmd = (file, size, destFile, gaus) => {
    return `convert ${file} -resize "${size}^"${
      gaus ? (' -gaussian-blur ' + GaussianBlur) : ''
    } -gravity center -crop ${size}+0+0 +repage ${destFile}`
}
// ...

使用:

node processImg.js images/**/*.jpg
2017-04-05 6 42 01

注: 使用通配符时,你获取的参数是通配符匹配的文件列表(如前面代码所示),如果你想获取原字符串, 请用引号,如node processImg.js "images/**/*.jpg"

2. git仓库更新后重新编译静态网站--githook + npm script

http://guide.cui.design/

这个是某种程度的css组件开发(专注CSS),一些我们机票部门的公用组件,比如paybar这些,可以通过这个项目有个 公共的最优实现,并在各个应用中保持一致。

以上并没有难点,难点主要在部署:即我们希望每次提交后(gitlab),可以在我们组的服务器同步最新的代码, 有最新的预览,并且这些应该完全自动化的。

代码的同步我们用了gitlab的API(这块是我同事在做),但预览呢?

  1. watch文件,有变动自动build。
  2. 使用githook,每次在服务器上仓库同步后自动build。

基于性能原因,第2种当然更好,所以我们选择hook仓库的post-merge(每册服务器本地仓库更新后调用)。

稍微看下post-merge的内容:

#!/bin/sh

cd /usr/share/nginx/html/repos/Dolphin-UI/
npm run build

不能更简单了,但的确做到了自动化和高效。

注: 一个坑,需要注意/usr/share/nginx/html/repos/Dolphin-UI/.git/hooks/post-merge文件的权限, 没有执行权限会导致脚本执行失败。

wechatimg243

3. sugar-cli--npm package

sugar-cli是我们组原型开发(除RN外)的工具,主要提供模版和css编译的功能。

http://cui.design/

背景和需求: 原来还是基于PHP那一套开发原型,比较笨重;新的开发环境希望基于node.js,有简单 但足够的模板语法,支持一种(或多种)css预处理语言,易于部署(预览)等等。

结合这些需求,最终开发了一个npm包sugar-cli,只要全局安装后,一个命令即可快速开始开发:

  1. 开箱即用,不需要其它依赖,基本不需要额外的配置(这也是为什么选择发布一个cli工具)。
  2. 类似handlebars的完善模板。
  3. postcss/sass/less全支持,完善的sourcemap。
  4. 支持livereload和dev server,改善开发体验。

这里就不具体描述功能了,下面主要讲讲怎么开发一个cli工具。

核心很简单:代码(含shebang line) + package.json配置。

下面是sugar-clisugar static(运行一个静态文件服务器)的实现:

#!/usr/bin/env node

const program = require('commander')

program
    .option('-a, --host <host>', 'server host, default to "0.0.0.0"')
    .option('-p, --port <port>', 'server port, default to 2333')
    .on('--help', () => {
        console.log(colors.green('  Examples:'))
        console.log()
        console.log(colors.gray('    $ sugar static'))
    })
    .parse(process.argv)

const root = program.args[0]

serveStatic(root, program.host, program.port)
// 这里省略 serveStatic 具体实现

然后,我们需要在package.json中配置:

  "bin": {
    "sugar": "bin/sugar.js"
  },

bin是个map,其中key是command,value 是对应可执行文件。当全局安装时,npm会 symlink 这个可执行文件到 prefix/bin;本地安装时, 则 symlink 这个可执行文件到 ./node_modules/.bin/。 (这一段可配合上面 全局安装的npm包为什么可以在shell里直接使用? 一起食用)。

结合这两个,我们即可轻松开发一个前端工具。剩下的我们可以发布到npm,然后请同学们试用即可。

2017-04-06 12 24 03

三. Thanks

Thanks

有问题直接问我即可。

kashtian commented 7 years ago

讲的很详细,学习了,谢谢