以我的小经验来看,软件萌新写出来的代码大多“无法直视”。具体现象包括空格和换行符乱用、文件夹和变量的命名多使用拼音等。坐不住的我,便想到了通过 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 <dir>')
.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 <dir>
命令。 -
.option()
方法:定义命令的具体选项。 -
.command()
方法:定义命令的子命令,这里是cs create <dir>
命令。 -
.action()
方法:用于设置命令执行的相关回调。这里绑定在cs create <dir>
命令上,在使用该命令时触发执行回调函数。
代码最后的 process
为进程对象,是 NodeJS 运行时存在的众多全局变量之一。process 对象中的 argv 属性用来捕获命令行参数。
3
刚刚在 bin/index.js
里说明的 .action
回调函数绑定在 cs create <dir>
命令下。当我们使用该命令时,会触发 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('\n项目目录开始创建\n')
create.init(distPath, dist, rootDir)
helpGuide()
}
不难理解,create
变量指向 cs create <dir>
所要执行的源代码;path
是 NodeJS 自带模块,提供文件目录解析功能。
最终 src/index.js
使用 exports.create
语句向外部暴露出 create
方法。bin/index.js
便可以将该方法通过 .action()
绑定到 cs create <dir>
命令上了。
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 国际许可协议】进行许可。