为什么自己写的组件库被引用总是报错——详解webpack的library和libraryTarget

如果我们仅仅是实现一个项目,我们大概率不会关注到webpack output中的这两个属性。但是如果我们是实现一个组件库,那么这两个属性就变得至关重要了。本文从自己之前遇到的一个问题说起,继而引申出library和libraryTarget属性。1. 故事起源当我自己开始写第一个组件库的时候,很快我就撸好了框架的代码,然后我兴致冲冲的把我的组件库引入到我的项目中,我记得那时候我是这么写的:组件库:import Feeds from ‘@/components/feeds/index’;export { Feeds,};主项目:import Feeds from ‘@/tencent/newsH5Ad’;// 一些其他代码<Feeds data=‘xxx’>然后我就收获了一个报错,Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: null。啊?难道是我的最终输出代码有问题?我检查了一下最终输出的代码,没有问题,Feed组件的代码也在里面。这个问题我查了很久,都没有答案,最后才发现是webpack打包的问题。这就涉及到了本文的主角,library和libraryTarget。2.2. library和libraryTarget我们都知道,webpack可以将不同的模块化方式(commonjs, AMD, CMD, ES6 Module)的代码打包。那我们打出来的代码包其实也可以按不同的模块化方式生成,所以:libraryTarget就是配置webpack打包内容的模块方式的参数而library就是webpack打包内容的名字所以library规定了组件库返回值的名字,libraryTarget规定了返回值的编码格式。libraryTarget的配置选项可以分为四大类:2.1 按不同的模块方式生成也就是我们这个问题的解决方法,由于我写的是一个React的UI组件库,所以我们需要commonjs的模块方式。因此只需要在webpack.config.js中配置这一项即可:module.exports = { entry: ‘./src/index.js’, output: { filename: ‘index.js’, // library: ‘MyLibrary’, // 模块名称 libraryTarget: ‘commonjs2’, // 输出格式 }, // 其他代码}事实上,你可以选择的选项有:commonjs/commonjs2: 将你的library暴露为CommonJS模块amd: 将你的library暴露为amd模块umd: 将你的library暴露为所有的模块定义下都可运行的方式其中AMD和UMD需要指定library,如果不声明组件库则不能正常运行。这是为了在浏览器上通过script标签加载时,用AMD模块方式输出的组件库可以有明确的模块名。如:define(“MyLibrary”, [], function() { return entry_return; // 此模块返回值,是入口 chunk 返回的值});注意:commonjs和commonjs2几乎相同,只不过commonjs只包含exports,而commonjs2还包含module.exports,所以直接使用commonjs2即可。2.2 生成为一个变量libraryTarget的默认值是var,顾名思义,就是将组件库入口起点的返回值生成一个变量。如:var MyLibrary = entry_return;也可以选择‘assign’,那样的话将默认生成和一个全局的变量。不管是var还是assign,都需要设置library的名称,否则就会报错。2.3 生成一个为一个对象的属性和第二种情况差不多,只不过会把这个变量赋值给某个对象,作为它的一个属性存在。可以选择的选项有:this: 返回值成为this的一个属性window: 返回值成为window的一个属性global: 返回值成为global的一个属性例如:this[“MyLibrary”] = entry_return;window[“MyLibrary”] = entry_return;global[“MyLibrary”] = entry_return;可以看到,这种情况下也必须指定library的名字。2.4 异步生成方式在这种情况下,libraryTarget的值为‘jsonp’,组件库入口起点的返回值,会被包裹到一个jsonp包装容器中,并配合webpack的externals使用——组件库的依赖由externals指定。如:MyLibrary(entry_return);3. 总结本文介绍了webpack中libraray和libraryTarget的相关内容,解释了为什么不设置它们时使用webpack打包出来的组件库会有问题。一般情况下,作为vue或者react组件库,libraryTarget在commonjs2,amd,umd中三者择其一即可。参考文献《webpack文档》 ...

January 21, 2019 · 1 min · jiezi

使用 TypeScript 编写一个完善包含测试、文档和持续集成的库

这篇文章主要是讲述如何使用 TypeScript 编写一个完善,包含测试、文档、持续集成的库,涵盖了编写整个库所需要的技术和工具,主要涵盖:项目目录骨架TypeScript 配置使用 jest 单元测试使用 vuepress 编写文档使用 github pages 部署文档持续集成部署原文首发于我的个人网站:听说 - https://tasaid.com/,推荐在我的网站阅读更多技术文章。前端开发 QQ 群:377786580 欢迎使用和了解滴滴金融出品的移动端组件库 Mand-mobile。为了迎合这篇文章,我编写了一个可以开箱即用的库模板:https://github.com/linkFly6/ts-lib-basic。里面集成了这篇文章所阐述的所有内容。初始化项目目录先初始化项目目录,一般来说,src 放源码,dist 放编译后的代码,tests 放单元测试,所以先初始化好基础目录。.├── .vscode # vscode 配置│ └── launch.json # vscode 调试配置├── dist # 编译产出目录,编译后才有├── src # 源码├── tests # 单元测试├── .gitignore # git 忽略文件├── .npmrc # npm 配置├── .travis.yml # github 持续集成├── LICENSE # 开源协议├── README.md # README├── package-lock.json # npm 锁定依赖├── package.json # npm├── tsconfig.json # typescript 配置└── tslint.json # tslint 校验先按照这个目录文件结构,然后我们会一步步填上内容。通过 npm init 初始化一个 npm 配置:初始化 TypeScript 相关工具既然包是基于 TypeScript 的,那么 TypeScript 工具必不可少。ts-node在开发中,可以使用 ts-node(可以理解为可以直接执行 ts 文件的 node)来直接运行我们的 ts 代码。npm i –save-dev typescriptnpm i –save-dev ts-node如果是 node 应用,为了让 TypeScript 能够进行 node 类型推导,则需要安装 Node 对应的类型声明:npm i –save-dev @types/nodetsconfig.jsontsconfig.json 是 TypeScript 的配置文件,这里提供一份可供参考是配置,置于项目根目录:{ “compilerOptions”: { “sourceMap”: false, “module”: “commonjs”, // 模块配置 “noImplicitAny”: true, // 是否默认禁用 any // “removeComments”: true, // 是否移除注释 “types”: [ // 默认引入的类型声明 “node”, // 默认引入 node 的类型声明 ], “baseUrl”: “.”, // 工作根目录 “paths”: { // / 指向 server/types,types 目录下都是 types 文件,所以不会编译产出 “/”: [ “./types/” ] }, “target”: “es6”, // 编译目标 “outDir”: “dist”, // 输出目录 “declaration”: true, // 是否自动创建类型声明 }, // 此配置生效范围 “include”: [ “src//” ],}tslint.jsontslint 类似 eslint,是 TypeScript 中的代码风格约束工具。关于 lint,个人方面比较倾向于非强制性的,所以只在 vscode 中安装了扩展 tslint,这样 vscode 会根据项目根目录配置的 tslint.json 标出不符合规范的信息。这里有一份推荐配置:{ “defaultSeverity”: “error”, “extends”: [ “tslint:recommended” ], “jsRules”: {}, “rules”: { “max-line-length”: [ true, 140 ], // 禁止内置原始类型 “ban-types”: false, // 禁止给参数赋值 “no-parameter-reassignment”: false, // 禁止空接口 “no-empty-interface”: true, // 显示类型代码就不需要再加类型声明了 “no-inferrable-types”: true, // 不允许使用内部模块 “no-internal-module”: true, // 不允许在变量赋值之外使用常量数值。如果未指定允许值的列表, 则默认情况下允许-1、0和1 => 乱七八糟的数字会让人混淆 // “no-magic-numbers”: [true], // 不允许使用内部 ‘modules’ 和 ’namespace’ “no-namespace”: true, // 非空断言,强制使用 == null 之类的断言 // “no-non-null-assertion”: true // 禁止 /// <reference path=>,直接用 import 即可 “no-reference”: true, // 禁止使用 require,应该使用 import foo = require(‘foo’) “no-var-requires”: false, // import 的顺序按照字母表 “ordered-imports”: false, // 对象属性声明按照字母表 “object-literal-sort-keys”: false, // // 结束语句后的分号 “semicolon”: [ false, “always” ], // 字符串强制单引号 “quotemark”: [ true, “single”, “jsx-double” ], // 禁止 arguments.callee “no-arg”: true, // if 语句的单行不用括号,多行用括号 “curly”: false, // 是否强制使用箭头函数,禁止匿名函数 “only-arrow-functions”: false, // 是否禁止多个空行 “no-consecutive-blank-lines”: false, // 在函数括号前要求或不允许空格 “space-before-function-paren”: false, // 箭头函数的参数使用括号 “arrow-parens”: [ true, “ban-single-arg-parens” ], // 不固定变量类型 “no-shadowed-variable”: false, // 行尾多余的空格 “no-trailing-whitespace”: false, // == 和 === “triple-equals”: false, // 禁止一些位运算符 “no-bitwise”: false, // 禁止 console “no-console”: false, // 检查变量名 “variable-name”: [ true, “ban-keywords” // “check-format”, // “allow-leading-underscore” ], // 一行声明变量表达式 “one-variable-per-declaration”: false, // 允许在一个文件里定义多个 class “max-classes-per-file”: [ true, 5 ], // 判断表达式 fn && fn() “no-unused-expression”: [ true, “allow-fast-null-checks” ], // 空函数 “no-empty”: false, // forin 是否必须包含 hasOwnProperty 判断 “forin”: false, “no-debugger”: false, // 强制要求必须要声明类型 “typedef”: [ true ] }, “rulesDirectory”: [ “./src” ]}package-lock.jsonpackage-lock.json 是 npm 5 之后引入的,为了解决 npm 过去使用的 package.json 版本依赖太宽松的问题。比如说 package.json 中依赖了包 mand-mobile,使用了最常用的插入依赖(^):“mand-mobile”: “^4.16.4”,假设自己项目在上线阶段, mand-mobile 更新到了 mand-mobile@4.17.0,而刚好 mand-mobile@4.17.0 又不小心出现了一个新 bug 会导致页面脚本错误。这时候上线安装依赖的时候,由于 package.json 和 ^ 约束太宽松,就会导致 mand-mobile@4.17.0 被安装,从而导致上线出问题。package-lock.json 就是为了解决这个问题,通过 npm 安装包的时候,会检测本地是否有 package-lock.json。如果没有 package-lock.json,就在安装包的时候将当前包依赖的详细信息(包括子级依赖)都写入生成 package-lock.json。如果有 package-lock.json,则根据 package.json,参考 pacakge-lock.json 来安装包依赖。来保证依赖稳定。本质上 ppackage-lock.json 的作用类似于 node_modules 包依赖的快照。单元测试一个合格的库应该包含完整的单元测试。这里我们使用 jest 对应的 TypeScript 版本:ts-jest。ts-jestts-jest 是 jest 的 TypeScript 支持版,API 和 jest 是一样的,它能够直接运行 .ts 为后缀的单元测试文件。安装 ts-jest 和对应的类型声明文件:npm i –save-dev jest #ts-jest 依赖 jestnpm i –save-dev ts-jestnpm i –save-dev @types/jest在 package.json 中加入 jest 配置和 npm run test 的脚本:{ “name”: “my-app”, “main”: “dist/index.js”, “scripts”: { “test”: “jest –verbose” }, “jest”: { “rootDir”: “tests”, “transform”: { “^.+\.tsx?$”: “ts-jest” }, “testRegex”: “(/tests/.|(\.|/)(test|spec))\.tsx?$”, “moduleFileExtensions”: [ “ts”, “tsx”, “js”, “jsx”, “json”, “node” ] }}这时候就可以基于 jest 编写单元测试了。在 tests/ 目录下加入 example.test.ts:import { isArrayLike } from ‘../src’describe(‘my-app:isArrayLike’, () => { test(‘isArrayLike(): true’, () => { expect( isArrayLike([]), ).toBe(true) }) test(‘isArrayLike(): false’, () => { expect( isArrayLike({}), ).toBe(false) })})然后执行 npm run test 即可看到单元测试结果。express 测试如果要测试 express/koa 之类的 web 应用框架程序,则可以使用 tj 大神的 supertest。安装对应的包:npm i –save-dev supertestnpm i –save-dev @types/supertestimport * as express from ’express’/ * 用于测试 express、koa 等 web 应用框架的工具 /import * as request from ‘supertest’import middleware from ‘../src’describe(‘my-app:basic’, () => { test(’locals’, done => { const app = express() app.use(middleware) app.get(’/example’, (req, res) => { res.send({ code: 0 }) }) // 使用 supertest 进行测试 request(app).get(’/example’).expect(200, { code: 0 }, done) })})debugdebug 也是 tj 大神编写的一个库,用于在应用程序中输出 debug 信息,用于调试工具库,著名的库大部分都采用该库进行 debug 支持。npm i –save debugnpm i –save-dev @types/debugimport * as d from ‘debug’const debug = d(my-app:basic)debug(‘debug info’)在启动应用程序的时候,只需要在环境变量中注入 DEBUG 即可:DEBUG=my-app node app.jsDEBUG=my-app* ts-node app.tsvscode 基于 ts-node 调试在 .vscode/launch.json 中可以配置基于 ts-node 的调试:{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 “version”: “0.2.0”, “configurations”: [ { “type”: “node”, “request”: “launch”, “name”: “启动程序”, // 基于 ts-node 调试 “program”: “${workspaceFolder}/node_modules/ts-node/dist/bin.js”, “args”: [ “-P”, “${workspaceRoot}/tests/tsconfig.json”, “${workspaceRoot}/tests/app.ts”, // 入口文件 ] } ]}文档文档方面,简陋一点的,可以直接使用 README,也可以用 gitbook。不过我个人方便比较推荐 vuepress。远程托管文档方面,要么自建服务器,要么直接托管到 Github 的 Pages。使用 vuepress 编写文档个人比较倾向于使用 vuepress 编写文档,是因为里面扩展 Markdown 扩展了许多丰富实用的语法,以及菜单结构的强大可配置。这里我们讨论的是在项目中集成文档。在项目根目录新建目录 /docsnpm i –save-dev vuepress在项目的 package.json 中加入脚本 “scripts”: { “docs”: “vuepress dev docs”, “docs:build”: “vuepress build docs” }在 /docs 新增文件 README.md,写入以下内容:—home: trueactionText: 开始使用 →actionLink: /readmefooter: MIT Licensed | Copyright © 2018-present linkFlyfeatures:- title: 快速 details: 快速创建库- title: 集成 details: 集成单元测试和自动化 doc 部署- title: TypeScript details: TypeScript 支持—集成了基础工具的,使用 TypeScript 快速编写一个应用库然后执行结合我们刚才配置的命令,执行 npm run docs,终端 shell 会输出 vuepress 启动的服务地址:访问地址,即可看到文档页面:使用 github pages 托管文档github pages 是 Github 提供的一个免费的页面托管服务,我们可以将 vuexpress 编译出来的文档托管到上面。Github Pages 服务和 Github 已经打通,可以从项目的 /docs 目录自动部署,这也就是我们为什么要在项目里新建 /docs 目录的原因。首先,我们将项目中 pageage.json 的脚本进行更新: “scripts”: { “docs:build”: “vuepress build docs && cp -rf ./docs/.vuepress/dist/* ./docs && rm -r ./docs/.vuepress/dist” }这段脚本的大体意思就是先使用 vuepress 构建产出文档的 HTML 文件(在 /docs/.vuepress/dist 目录下),然后将 dist 目录移动到 docs/ 目录下,因为 Github Pages 在识别 docs/ 的时候只能识别 docs/index.html。执行 npm run docs:build。将本地的项目 push 到 Github 以后,打开该项目的 Setting:在 Github Pages 配置项选择 docs/ 文件夹:然后访问 https://<USERNAME or GROUP>.gitlab.io/<REPO>/ 即可看到自动部署的文档。例如:https://linkfly6.github.io/ts-lib-basic/。使用持续集成服务 travis-citravis-ci 是一个持续集成服务,它可以用来自动部署和构建 Github 上的项目。我们可以集成我们的单元测试。在项目根目录加入 .travis.yml,在 master 分支进行提交的时候自动运行 npm run test 命令(npm run test 命令配置参见 ts-jest 章节):sudo: falselanguage: node_jsnode_js: - “8"cache: directories: - node_modulesbranches: only: - masterscript: npm run test打开 https://travis-ci.org/ 进行注册或登录。新增接入的项目:选择要打开持续集成的项目:然后我们更新文档或代码,提交代码到 Github。稍等大概几十秒,就可以在 travis-ci 里面看到自己的单元测试任务:最后,在测试完毕的情况下,在 https://www.npmjs.com/ 进行注册。在 npm 的源是官方的(npm config set registry https://registry.npmjs.org/)情况下,执行 npm login 登录 npm 以后,npm publish 发布包即可。最后,为了迎合这篇文章,我编写了一个可以开箱即用的库模板:https://github.com/linkFly6/ts-lib-basic。里面集成了这篇文章所阐述的所有内容。前端开发 QQ 群:377786580 欢迎使用和了解金融出品的移动端组件库 Mand-mobile。原文首发于我的个人网站:听说 - https://tasaid.com/,推荐在我的网站阅读更多技术文章。 ...

January 3, 2019 · 5 min · jiezi