资讯专栏INFORMATION COLUMN

造轮子之 npm i -g creatshare-app-init 源码浅析

adie / 1806人阅读

摘要:刚刚在里说明的回调函数绑定在命令下。使用开源协议源代码都放在目录下目录要对不同的代码进行合理的分层。,我是韩亦乐,现任本科软工男一枚。

以我的小经验来看,软件萌新写出来的代码大多“无法直视”。具体现象包括空格和换行符乱用、文件夹和变量的命名多使用拼音等。坐不住的我,便想到了通过 ESLint 配置文件来规范实验室的 JavaScript 代码规范的 Idea。

于是巧遇前实验室毕业学长曾经发布的 npm 包——creatshare-project-quick-init。安装好这个包,我们便可以在空文件夹下生成一个项目的基础骨架。

dist  //发布目录,用于生产环境
src   //开发目录,开发时所需资源
|----dist  //测试环境目录
|     |----static
|             |----css  //编译打包后的css资源
|             |----js   //打包压缩后的js资源
|             |----imgs //测试环境图片资源
|----less  //开发所需less代码
|----js    //开发所需js代码
|    |----lib //库或框架资源
|----imgs  //开发所需图片资源
index.html    //开发页面
gulpfile.js
package.json
README.md

What a good idea~!

在学长的这个包中,主要构建了 gulp 配置,less 和测试文件的骨架。虽然再无更多内容,但这份构建基础骨架的灵感还是被我愉快的收走了——学前端的人很多,但大多都太缺工程化意识了。于是,这个灵感成为了不错突破口。

creatshare-app-init 脚手架孕育而生。

0

通过这篇文章,你能了解到:

如何用 NodeJS 编写命令行工具?

如何发布自己的 npm 包?

笔者与 creatshare-app-init 的故事?

在本文中,或多或少出现过以下关键字,我的解释是:

轮子:该词在前端开发日常用语中,表示一个基于原生代码实现,但并没有对前端行业产生积极意义的模块。虽然它的出现方便了一些人的使用,但更多的加大了我们的学习成本。

项目:该词在前端领域常指一个服务于用户的软件立项。

模块:creatshare-app-init 就是一个模块,是开发前端项目中的一个子集。正如汽车的各个部件一样,多个模块合理组装起来才是一辆汽车。

1

尝试解析源码,第一步,从模块根目录下的 package.json 来看。

"dependencies": {
    "commander": "^2.11.0"
},
"devDependencies": {
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-runtime": "^6.26.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-node": "^5.2.1",
    "eslint-plugin-promise": "^3.6.0",
    "eslint-plugin-standard": "^3.0.1"
}

如上,dependencies 声明了模块上线时的依赖,devDependencies 声明了模块开发时的依赖。该模块在上线时,即 npm 包被用户用到时,只需要 commander 库。commander 库是 NodeJS 命令行接口开发的优选解决方案,受启发于 Ruby 的 commander。在解析 bin/index.js 源码时将详细拓展。

"name": "creatshare-app-init",
"version": "2.1.0",
"description": "CreatShare 实验室前端项目初始化工具",
"bin": {
  "cs": "bin/index.js"
},
"scripts": {
  "compile": "babel src/ -d lib/",
  "prepublish": "npm run compile",
  "eslint": "eslint src bin",
  "test": "echo "Error: no test specified" && exit 1"
},

上面一段是 package.json 最开头的内容,字段详情如下:

name 字段:声明模块名称。特殊注意该字段不允许大写字母及空格的出现,且其与 version 字段形成了 npm 模块的唯一标识符。

version 字段:声明模块当前版本号。这里每当使用 npm publish 将模块发布到 npm 仓库中时,版本号都需要手动自增。

description 字段:对模块进行描述,同时有助于被检索。

bin 字段:npm 本身是通过 bin 属性配置一个或多个可解析到 PATH 路径下的可执行模块。模块若被全局安装,则 npm 会为 bin 中配置的文件在 bin 目录下创建一个软连接;模块若被局部安装,软连接会配置在项目内的 ./node_modules/.bin/目录下。

script 字段:定义模块的脚本配置。如,当我们在模块目录下使用 npm run compile 时,将自动执行 babel src/ -d lib/ 命令,进行 ECMAScript6 代码的转译。

2

刚刚提到 package.json 配置文件下的 bin 字段声明了 npm 在生成软连接时的配置。这就便是用户在安装好这个目录后,可以随时使用 cs 命令的出处。

我们又提到了该模块在非开发环境下只需用到 commander 模块,这个模块是 NodeJS 命令行接口开发的优选解决方案。

基于这俩点,我们就从 bin 字段所指向的 bin/index.js 聊起。

#!/usr/bin/env node

var program = require("commander")
var cs = require("../lib/cs")

program
  .allowUnknownOption()
  .version("2.1.1")
  .description("CreatShare 互联网实验室前端 Web App 项目脚手架")
  .option("-e, --enjoy")

program.
  .command("create ")
  .description("创建一个新的 Web App 项目骨架")
  .action(function (rootDir) {
    cs.create(rootDir)
})

program.parse(process.argv)

就这么二十来行。因为我们要写的模块是要运行在命令行下的,就需要 #!/usr/bin/env node 语句来告诉系统使用 node 环境来运行我们的文件,必不可少。

在引入 commander 并将其赋值给 program 变量后,我们对其使用了如下方法:

.allowUnknownOption() 方法:

.version() 方法:用于设置命令程序的版本号。

.description() 方法:用于设置命令的描述。可以绑定在跟命令下,这里是 cs 命令;或绑定在子命令下,如 cs create

命令。

.option() 方法:定义命令的具体选项。

.command() 方法:定义命令的子命令,这里是 cs create

命令。

.action() 方法:用于设置命令执行的相关回调。这里绑定在 cs create

命令上,在使用该命令时触发执行回调函数。

代码最后的 process 为进程对象,是 NodeJS 运行时存在的众多全局变量之一。process 对象中的 argv 属性用来捕获命令行参数。

3

刚刚在 bin/index.js 里说明的 .action 回调函数绑定在 cs create

命令下。当我们使用该命令时,会触发 cs.create() 语句的执行,这就要提及我们引入的 lib/cs.js 文件了。

打住,第一节里展示的 package.json 中,script字段里有这么一条语句:"compile": "babel src/ -d lib/"。这是说明 lib/ 文件夹下的代码是通过 src/ 文件夹下的代码转译过来的,真正我们需要去关注的是 src/cs.js 文件。

为什么需要转译?src 里的 JavaScript 代码或多或少的使用到了 ECMAScript6 新特性,有些用户的 Node 环境并不一定能得到较好的解析。

src/cs.js 主要代码片段为:

let create = require("./create")
let path = require("path")
let distPath = path.join(__dirname, "/../dist")
let dist = process.cwd() + "/"

/**
* [运行 create 命令]
* @return {[type]} [description]
*/
exports.create = (rootDir) => {
  console.log("
项目目录开始创建
")
  create.init(distPath, dist, rootDir)
  helpGuide()
}

不难理解,create 变量指向 cs create

所要执行的源代码;path 是 NodeJS 自带模块,提供文件目录解析功能。

最终 src/index.js 使用 exports.create 语句向外部暴露出 create 方法。bin/index.js 便可以将该方法通过 .action() 绑定到 cs create

命令上了。

4

精彩的来了。都说 ECMAScript6 的指定振奋人心,JavaScript 的魅力越来越大,这里便是一次体验 JavaScript 在 NodeJS 上的新玩法有趣之旅。

src/create.js 文件中,主要用到了 NodeJS 自带的 fs 文件模块,来生成新项目的基础架构。文件最后暴露出的 init 方法源码如下。

exports.init = (path, dist, rootDir) => {
  createRootDir(rootDir)
  // 从新目录开始新建项目
  dist = dist + rootDir
  copyDir(path, dist)
}

init 方法获取了 path 参数、dist 参数和 rootDir 参数。在该方法中,我们先将 rootDir 参数传入 createRootDir() 函数中创建项目根目录。

在哪里创建项目根目录呢?就在执行 cs 命令时的当前目录下:

const createRootDir = (rootDir) => {
  fs.access(process.cwd(), function (err) {
    if (err) {
      // 目录不存在时创建目录
      fs.mkdirSync(rootDir)
    }
  })
}

有了项目根目录,就要将模块下 dist/ 文件夹里的所有文件递归拷贝到根目录下。一个参数用来指向 dist/ 文件夹,另一个参数用来指向根目录,便可以开始递归复制。

/**
 * [初始化静态资源]
 * @param  {[type]} src  [初始化资源路径]
 * @param  {[type]} dist [当前终端所在目录]
 * @return {[type]}      [description]
 */
const copyDir = (src, dist) => {
  fs.access(dist, function (err) {
    if (err) {
      // 目录不存在时创建目录
      fs.mkdirSync(dist)
    }
    _copy(null, src, dist)
  })

  function _copy (err, src, dist) {
    if (err) { throw err }
    fs.readdir(src, function (err, files) {
      if (err) { throw err }
      // 过滤不生成的文件
      miscFiles.forEach(function (v) {
        if (!files.includes(v)) return
        files = files.filter(function (k) {
          return k !== v
        })
      })
      // 遍历目录中的文件
      files.forEach(function (path) {
        var _src = src + "/" + path
        var _dist = dist + "/" + path
        fs.stat(_src, function (err, st) {
          if (err) { throw err }
          // 判断是文件还是目录
          if (st.isFile()) {
            fs.writeFileSync(_dist, fs.readFileSync(_src))
          } else if (st.isDirectory()) {
            // 当是目录是,递归复制
            copyDir(_src, _dist)
          }
        })
      })
    })
  }
}

fs 文件模块的具体内容推荐阅读阮一峰的开源电子书——《JavaScript 标准参考教程》中的“NodeJS”章节,来深入浅出 fs 模块的用法。

完美,这时我们就可以发布我们的脚手架包了。

5

如何发布一个 npm 包到 npm 仓库中,供其他人使用?当我们照着第一步,将 package.json 配置好后,其实模块的准备工作已经做好了。

还没有做的就是在域名为 npmjs.com 的官网上注册一个账号。这样,当我们直接在模块根目录使用 npm publish 命令的时候,输入正确的 npmjs.com 账号、密码,就能成功发布你的开源包了!

纵然读博文是一个有趣的体验,但也可以亲自动手试一试哦。

6

也就是说,酷炫的生成新项目骨架的来源,只是简单的递归复制该模块下的 dist/ 文件夹到新项目中。但我们需要关注的重点在于,dist/ 文件夹下,到底装了什么?

“初级 Web App 项目初始化工具”一说,也就名归有主了。dist/ 模板,也就是新项目的骨架如下。

.
├── .babelrc             # ES6 代码转义规则配置
├── .eslint.js           # JavaScript 代码规范
├── .gitignore           # Git 不跟踪的特殊文件
├── LICENSE              # 开源协议
├── README.md            # 项目介绍
├── material             # README.md 引用的图片库
├── package.json         # 项目配置文件
├── src                  # 源码开发目录
│   ├── favicon.ico      # 网页标题小图标
│   ├── html             # HTML 页面模板目录
│   ├── image            # 图片资源目录
│   ├── manifest.json    # 网络应用清单
│   ├── script           # 脚本文件资源目录
│   └── style            # 样式文件资源目录
├── webpack.config.js    # Webpack 多文件打包基础配置
├── webpack.dev.js       # Webpack 开发环境配置
├── webpack.prod.js      # Webpack 发布上线配置
└── yarn.lock            # yarn 包管理器的依赖说明

新项目骨架中默认推荐了:

使用 Webpack 来打包多页面;

使用 ESLint 来规范自己项目的 JavaScript 代码;

使用 Babel 来编译使用 ECMAScript 新特性的 JavaScript 代码。

使用 MIT 开源协议;

源代码都放在 src/ 目录下;

src/ 目录要对不同的代码进行合理的分层。

End

现在的不足,是未来的畅想。

这个模块并不完美,一个健壮的命令还应该能支持足够多的参数,运行足够有意义的子命令。比如我们常用 man 命令来看另一个命令的使用手册,那要让用户能用到 man cs 命令,还需要我们在代码中加入 man 字段等等。。

我又为什么,这么热衷于分享这个轮子?

记得有一个前端群里曾有人问过:

“怎么没有 VueJS 的源码解析?”

时,我说过:

“大牛很忙,关注的是前端前沿,不写这些源码解析博文是个好事。

“当我们想有一个源码解析教程的时候,这是一个打开新世界的契机——未尝不使我们亲自来写,通过分享走向学习效率金字塔的最高层?”

这样的能力并不是人人都能具备,也不必要让人人都具备。我曾在大一傲气的说过“做最好的自己,影响该影响的人”,现在想起来除了有立刻找地洞钻进去的冲动外,反而还是觉得有一定的道理(笑。这时候允许我自称为一次“教主”,我们的理念是:

读文档,读文档,读文档。
写博客,写博客,写博客。


Hello,我是韩亦乐,现任本科软工男一枚。软件工程专业的一路学习中,我有很多感悟,也享受持续分享的过程。如果想了解更多或能及时收到我的最新文章,欢迎订阅我的个人微信号:韩亦乐。我的简书个人主页中,有我的订阅号二维码和 Github 主页地址;我的知乎主页 中也会坚持产出,欢迎关注。

本文内部编号经由我的 Github 相关仓库统一管理;本文可能发布在多个平台但仅在上述仓库中长期维护;本文同时采用【知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议】进行许可。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89479.html

相关文章

  • 浅析webpack源码convert-argv模块(二)

    摘要:接下来我看看一下函数我们先按照分支走为读取是里的对象,饶了这大的一个圈子,那么接下来一起来看一看对你的输入配置做了怎么样的处理吧 打开webpeck-cli下的convert-argv.js文件 // 定义options为空数组 const options = []; // webpack -d 检查 -d指令 if (argv.d) { //... } ...

    lemon 评论0 收藏0
  • 浅析webpack源码前言(一)

    为什么读webpack源码 因为前端框架离不开webpack,天天都在用的东西啊,怎能不研究 读源码能学到很多做项目看书学不到的东西,比如说架构,构造函数,es6很边缘的用法,甚至给函数命名也会潜移默化的影响等 想写源码,不看源码怎么行,虽然现在还不知道写什么,就算不写什么,看看别人写的总可以吧 知道世界的广阔,那么多插件,那么多软件开发师,他们在做什么,同样是写js的,怎么他们能这么伟大 好奇...

    suosuopuo 评论0 收藏0
  • 一言不合轮子--撸一个ReactTimePicker

    摘要:时间选择的表盘其实有两个,一个是小时的选择,另一个则是分钟的选择。也就是说,第一步选择小时,第二部选择分钟它是一个小时制的时间选择器。而则用于处理拖拽事件,标记着当前是否处于被拖拽状态。 本文的源码全部位于github项目仓库react-times,如果有差异请以github为准。最终线上DEMO可见react-times github page 文章记录了一次创建独立React组件...

    lifesimple 评论0 收藏0
  • 轮子 - EGGJS的MySQL操作库

    摘要:最近学习,学习过程中使用官方推荐的库,感觉官方库不太好用,基础的没问题。介绍这个轮子其实是很早以前就造好的,主要参考的数据库操作方式。将设置表名设置查询字段联表等操作进行链式操作,给人一种语义化操作数据库的感觉。 最近学习eggjs,学习过程中使用官方推荐的MySQL库,感觉官方库不太好用,基础的CURD没问题。但是复杂点的操作就不行了,虽然官方还有一个egg-sequelize,但是...

    Alex 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<