从0到1封装一个库来学习JavaScript(ES6)以及Webpack


代码

Anikyu源代码仓库:anikyu

先从传说中的Vue CLI脚手架开始

毕业后我来了北京。到现在作为正式员工(2020年4月),我去过两个公司,都是初创公司,用的技术栈基本都是Vue.js这一套,通过Vue CLI工具即可搭建一个脚手架(空白项目/项目模板),不用任何配置,在初始化过后,脚手架就可以被启动了,然后我们可以在此基础上进行开发。

vue init webpack
image

之后我们进入项目目录,可以看到如下的文件结构:
image

事实上,我们在此关心的内容并不多,仅仅是了解一下目录结构,以及该项目使用到了哪些依赖包。

脚手架文件结构大致了解

参考自vuejs-templates webpack

  • build/ – webpack相关配置
  • config/ – 有关项目的一些配置
  • src/ – 写项目业务代码的地方
  • static/ – 保存静态文件的地方
  • test/ – 写测试的地方
  • .babelrc – Bebel配置
  • .editorconfig – 编辑器配置
  • .eslintrc.js – ESLint配置
  • .eslintignore – ESLint忽略规则
  • .gitignore – Git忽略规则
  • index.html – index.html文档模板
  • package.json – 项目信息、构建脚本及依赖项
  • README.md – 自述文件

package.json 相关内容

package.json包含有当前项目的信息。

image
  • name – 项目名称
  • version – 版本
  • description – 项目描述
  • author – 作者
  • private – 私有标识(设为true以防止项目被意外发布到npm)
  • scripts – 脚本
  • dependencies – 运行依赖项
  • devDependencies – 开发依赖项
  • engines – 引擎版本
  • browserslist – 浏览器列表

由此我们可以了解到,我们使用Vue CLI工具创建的项目:

  • 在生产环境运行时依赖于vue和vue-router这两个包。
  • 在开发环境下需要的包很多(主要是和前端工程化相关的内容),大致归纳一下包括几个大类:
    • Babel
    • ESLint
    • Vue
    • Webpack
    • 其他工具

注:在最终打出来的生产环境包中仅包含运行依赖项,不会包含开发依赖项。

准备开始

上面我们所看的是Vue脚手架初始项目的依赖,使用脚手架靠发的最终产品实质上是一个面向最终用户、基于Vue的项目,而我们的目标是构建一个面向开发者的JavaScript的库。

事实上,我们开发自己的库并不用像上面的脚手架一样需要很多依赖,准确来说其实可以不必任何开发依赖或是运行依赖。我们完全可以不借助任何前端工程化工具,手写代码,直接发布。唯一的问题是代码可能会稍显冗余,或不太严谨,或者不管遇到什么问题都得手动改代码。

由于前端工程化是趋势,本文的目的也主要是学习这些工程化工具。因此,借助上文中我们所提及的一些工具:

  • Babel – 用于将ES6代码转译为ES5代码
  • ESLint – 用于确保代码风格符合规范
  • Webpack – 用于生成最终包

我们就可以构建一个无需任何运行依赖项即可运行的库。

既然有了对Vue脚手架初始项目文件结构及其依赖项的大致了解,我们是不是就可以依葫芦画瓢,来实现一个自己的库呢?

这里,以我之前所写的Anikyu这个库为例来进行介绍。

NPM配置

初始化项目

cd 到将要用于存储项目的空目录

使用命令

npm init

填写一些信息,生成一个package.json
image

package.json 文件的全部内容已经展现在命令行窗口中,其中包含有在上文没有提及的一些字段:

  • main: 文件入口(即在项目中引入库时实际引入的文件,例如ES6语法 import Anikyu from 'anikyu'
  • repository: 代码仓库相关信息
  • keywords: 项目关键词(可以用于优化npm搜索)
  • license: 项目许可证

这样我们就创建了一个空白项目。

如果我们不需要使用工程化工具,只需在当前目录下创建一个index.js文件(对于package.js的入口文件),然后将代码写在其中即可。

与此同时,别忘了在项目文件夹里初始化一个Git仓库,以确保项目文件出现问题时可以从仓库中找回。

安装一些开发依赖

可以将下文提及的相关开发依赖复制到package.json中devDependencies字段下,之后运行npm i 来直接安装;或者在命令行中手动运行npm i --save-dev+依赖名称 进行安装。(由于在中国直接访问npm的速度很慢,因此这里使用了cnpm)

image

ESLint相关

    "eslint": "^6.8.0"

ESLint 相关依赖以及配置可以先全局安装ESLint,然后进入项目文件夹,通过eslint --init来初始化。
下图是初始化结束后发生的相关变化。
image

    "eslint-plugin-import": "^2.20.1",
    "eslint-plugin-node": "^11.0.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.1"

若初始化配置的时候选择了相关内容,同时也会安装相关依赖(此处暂未了解,今天看项目的时候发现多了这四个依赖,建议先别复制)

Webpack相关

    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "babel-loader": "^8.0.6",
    "@purtuga/esm-webpack-plugin": "^1.2.1",
    "clean-webpack-plugin": "^3.0.0"
  • webpack、webpack-cli
    Webpack的核心;不知为何似乎两个都需要安装,不安装其中一个会报错?
  • babel-loader
    用于在Webpack中对js文件使用Babel进行处理
  • @purtuga/esm-webpack-plugin
    用于将库打包为符合ES Module的包
  • clean-webpack-plugin
    打包之前将打包目标文件夹(/dist)进行清空

Babel相关

    "@babel/cli": "^7.8.3",
    "@babel/core": "^7.8.3",
    "@babel/polyfill": "^7.8.3",
    "@babel/preset-env": "^7.8.3"

(暂未了解)

规划项目文件结构

通过上文我们对Vue CLI脚手架的了解,我们也可以在我们的库中规划出文件结构。

Anikyu是这样规划的:

  • demo/ – 包含一些使用了该库的DEMO,让用户通过运行该文件夹里的文件,了解你的库可以做什么
  • dist/ – 用于保存打包后产生的文件
  • src/ – 写Anikyu代码的地方
    • polyfill/ – polyfill相关代码
      • requestAnimationFrame.js requestAnimationFrame polyfill
    • anikyu_class.js – 定义Anikyu类
    • anikyu.js – Anikyu的入口文件,同时混入polyfill
    • easing_funcs.js – 定义缓动函数
    • event_doer.js – 定义EventDoer类
    • executor.js – 定义动画执行函数
    • util.js – 定义一些工具类函数
  • CHANGELOG.md – 项目变更日志
  • LICENSE – 许可
  • README.md – 英文自述文档
  • README.zh-CN.md – 中文自述文档

规划代码结构

Anikyu库代码使用ES6语法来进行组织,例如引入(import … from …)、导出(export …)、类(class)。

由于Anikyu核心代码之前已经写好(我这里就不再从头写一遍了),因此我们可以将项目中src目录拷贝到新项目根目录下。

image

同时也修改一下package.json中的入口文件为src/anikyu.js。此时这就是一个未进行打包的、由很多零散文件所组成的库。如果你的浏览器支持运行ES Module,那你将能够在浏览器中直接运行这个库。

image

执行器(executor.js)

执行器是Anikyu计算补间的核心,目标对象中值的计算、改变,以及事件的触发也由执行器来进行。

缓动函数(easing_funcs.js)

缓动函数来自ECharts中的相关示例。

Anikyu类(anikyu_class.js)与EventDoer类(event_doer.js)

Anikyu是一个动画对象,那对于动画状态的监听(例如监听动画帧的请求、动画的结束)使用和事件相类似的机制会更好一些。

Anikyu类基于EventDoer类,继承关系如图所示:

image

EventDoer类似浏览器中自带的EventTarget对象,可以为Anikyu对象添加事件监听。当Anikyu示例的某一动画阶段正在请求帧或是播放完成的时候,能够触发相关事件监听函数。

Anikyu类则用于控制动画的播放过程,包括暂停、继续、废弃等等。

工具函数(util.js)

包含了一些常用工具,如计算CSS实际值、事件触发、生成范围内随机数、数值限制、时间获取。

polyfill

当前仅包含了requestAnimationFrame的polyfill,以兼容IE9浏览器。

Anikyu(anikyu.js)

该文件是Anikyu的出口,用于进行混入polyfill等操作。

使用Webpack进行打包

webpack试用

如果你早前对Webpack进行过全局安装(即只需在运行框/cmd.exe中输入webpack不会报找不到命令),那在这一步骤中,你只需在命令行中输入:

webpack ./src/anikyu.js

即可完成打包,打包好的文件默认保存在dist目录下,文件名为main.js。
image

通过这种方式打包,我们发现以下几个问题:

  1. 文件是默认被压缩的
  2. 引入该文件后,其中的属性、方法似乎无法以预想的方式通过ES Module或传统script被访问到

因此,我们还需要对Webpack进行深入配置。

打包为符合umd规范的包

( 参考Webpack官网 – Authoring Libraries

此时,我们在根目录创建一个webpack.config.js,在其中写入Webpack配置。

image

这里的module.exports可以接收一个配置对象(只打包一个文件),也可以接收由多个类似的配置对象组成的数组(打包多个文件)。这里我们创建Anikyu 一种版本的两个文件 —— 经过压缩的文件(anikyu.min.js)和未经压缩的文件(anikyu.js)。两个文件都符合umd规范,即能够在不支持ES Module的浏览器中直接运行,区别仅在于代码是否被压缩。

我们看一看配置对象,如下是未压缩的UMD版本的配置。

{
    entry: './src/anikyu.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'anikyu.js',
        library: 'Anikyu',
        libraryTarget: 'umd',
        libraryExport: 'default',
        globalObject: 'this'
    },
    mode: 'production',
    optimization:{
        minimize: false
    }
}
  • entry – 入口文件。Webpack能够根据该文件自动层层查找在该文件中引入的文件,然后统一打包
  • output – 输出配置
    • path – 输出目录
    • filename – 输出文件名
    • library – 库的名称。用户引入库后将以该名称进行调用
    • libraryTarget – 库的目标格式。表示库能够以哪些方式导入,这里值为umd,表示库符合umd规范。
    • libraryExport – 对外暴露的库的属性
    • globalObject – 运行环境全局对象的名称
  • mode – 打包环境( development 或 production )
  • optimization – 打包优化
    • minimize – 是否压缩代码(默认为true)

之后我们在根目录下,不带参数直接执行webpack命令,文件即可开始打包。

打包为能够通过ES Module引入的包

上一步中打的包符合umd规范,能够在浏览器中通过传统的script标签进行引入。但根据我的观察,很多类库(如Vue.js、Three.js)都提供了支持ES Module的包,事实上这似乎也正在成为一种趋势。经过本人各种百度,貌似让Webpack打出ES Module包的方法是引入EsmWebpackPlugin扩展(来自@purtuga/esm-webpack-plugin包)。

我们将该扩展引入到webpack.config.js中

image

在plugins字段中引入,libraryTarget改为var。之后我们再进行打包,即可打包出ES Module包,实际测试,一切正常。

在Webpack中使用Babel

配置webpack.config.js

在之前安装依赖的过程中,我们已经安装过了babel-loader,和其它各种各样的loader一样,它处理的是js文件(虽然Webpack原生支持处理js,但相关不兼容老旧浏览器的代码并没有经过转译过程)。
image

在webpack.config.js中的module字段里添加rule,表示遇上js文件时就使用babel进行处理。

我认为,在浏览器环境下,原生支持ES Module的浏览器必然也支持Anikyu中所使用的相关ES6特性,因此ES Module包我没有使用Babel,仅对umd包使用Babel。

配置.babelrc

这是Anikyu的配置,但对于其具体配置详情本人目前暂无了解。

请参阅 Config Files · Babel

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "targets": {
          "browsers": ["last 2 versions", "ie >= 9"]
        }
      }
    ]
  ]
}

在配置完成后,我们可以重新运行webpack进行打包,此时Babel就可以对不兼容老旧浏览器的代码进行转译,使得库可供不支持ES6等特性的浏览器使用。


行文至此,Anikyu库的开发、打包实际上已经可以告一段落,但开发过程中有的地方还可以继续优化,例如代码风格可能还不够规范、每次运行打包都要输入webpack不太方便。

那接下来的步骤我们就对这些细节进行优化。


配置ESLint

上面的步骤中我们已经安装好了ESLint,生成了.eslintrc.js这个配置文件,其中的配置都是在执行初始化ESLint命令后根据你的选择所生成的,此时ESLint规则便已经生效。

我在该文件中的“rules”加入了额外的一些规则,以符合我自己写代码的习惯。

"rules": {
    "indent": [
        "error",
        "tab"
    ],
    "linebreak-style": [
        "error",
        "windows"
    ],
    "quotes": [
        "error",
        "single"
    ],
    "semi": [
        "error",
        "always"
    ],
    "space-before-function-paren": 1,
    "space-infix-ops": 1,
    "spaced-comment": 1
}
  • 缩进:tab
  • 换行符:windows风格
  • 引号:单引号
  • 分号:必须
  • 函数声明语句(左括号之前)前的空格:1
  • 运算符空格:1
  • 注释符号(//)后的空格:1

如有文件无需被ESLint检查,可在.eslintignore里设置忽略。

配置 NPM 脚本

在我们日常开发项目过程中,例如我们要打包一个项目,一般会执行npm run build,而不是手动执行webpack。要对此进行配置,我们需要修改package.json中的script。

image
{
    "test": "echo \"Error: no test specified\" && exit 0",
    "lint": "eslint src --ext js",
    "build": "webpack-cli"
 }

在这里,我们添加了lint和build两个脚本,原有的test脚本由于我不会配置,所以先让它 return 0

  • lint – 用于检查代码风格,发现问题时会报错
  • build – 用于执行代码打包

发布到NPM

到此,Anikyu库的开发已经结束,假设现在经过测试,一切运行正常,我们就可以对包进行发布。

  • npm | build amazing things自行注册一个npm账号(如有可跳过)
  • 在命令行运行npm login,输入登录凭据来登录
    image
  • 登陆完成后,执行npm publish,即可将包发布到NPM
    image

(尴尬了,刚刚不慎把这里的demo版本发布出去了,本来当前线上版本是0.2.2,这里初始化以后默认版本是1.0.0,忘改了;不过还好我及时用 npm unpublish --force 撤回了刚刚的发布)

从此,世界各地的人将能够通过npm install anikyu --save来安装Anikyu依赖。

开发中经历的一些小事情

我想起啥的时候就写些啥吧。。。

是通过直接传入还是使用类似事件的机制来处理动画播放期间要执行的函数?

早期开始做这个库的时候,我试过直接在配置中传入函数作为参数,例如:

new Anikyu({
    onAnimate: function(){...},
    onFinish: function(){...}
})

但这样做存在的问题是,如果需要在事件被触发后执行多个函数,这种方式不是很灵活。

正如很久以前在DOM文档里写相关事件处理函数:

window.onload = function (event){ ... }

因此我尝试让Anikyu直接继承浏览器自带的EventTarget对象(该对象提供了我们所熟知的.addEventListener等方法)。在不同浏览器上进行测试后,发现任何版本的IE浏览器都无法通过 new EventTarget() 的方式来调用。在继续测试、查阅文档过程中,发现EventTarget类并不能够支持Anikyu所需的所有API。

最终我编写、模拟了一个和EventTarget类相似的EventDoer类,由Anikyu类继承。

在Anikyu实例上可这样调用:

let ani = new Anikyu(...)
ani.addEventListener('animate',function(e){
...
})

参考自EventTarget – Web APIs | MDN

是使用Rollup还是Webpack?

前期没做过深入了解,只是发现Webpack用途广泛(日常项目以及招聘信息等很多地方都提到这个,顺便也学一下),于是就尝试使用Webpack来进行打包。但Webpack似乎有个问题,在IE8下,某个地方会提示无法使用Object.defineProperty方法(可能和Vue.js不支持IE8是同一个原因),导致报错。但我自己写的代码里似乎没用到Object.defineProperty方法,定眼一看,代码似乎来自于Webpack(后来在官网发现Webpack的确只能够兼容到IE9),遂考虑更换一个打包工具。

到后面瞄了一下Three.js、Vue.js和ECharts的打包工具,用的都是Rollup。在某个分支里我也对其进行了配置,但问题就在于:

  1. 代码压缩配置不对
  2. Babel配置不对
  3. 打包后代码莫名运行不了

最终,遂暂时弃疗Rollup。

完结撒花~

历时两天,整篇文章终于写完了。这大概就是从0到1搭建一个前端工程化项目的过程吧。不过本人目前还是处于很菜的状态,不是很确定上文的相关表达有没有很准确、很通俗易懂。如果你对Anikyu这个库很中意,不妨拿来用一用吧。发现问题,欢迎提issue。

提示一下

Anikyu是一个面向本人兴趣编程的项目,而不是面向公司KPI编程的项目。


评论