文章目录
概述
目的
typescript作为未来前端开发的主流语言,在前端开发的过程中扮演着越来越重要的角色。如果有的同学平时的主要开发内容都是处理业务逻辑,那么可能没有那么容易体会到typescript带来的好处,如果有的同学经常会造一些“轮子”,也就是一些复用性比较强的库(工具函数库或者组件库),那么typescript绝对会给你带来工作效率的提升。因为typescript的类型提示以及类型约束,能够大幅度地减少bug,使得功能结构清晰,这是ES6,ES7无法具备的优点。
内容
本文的内容是,基于vue-cli3脚手架创建的typescript工程,编写一些简单的typescript函数,打包为umd模块,发布到npm上,然后在浏览器环境、node环境、es6模块以及typescript模块中使用这个umd包,在typescript中具有类型提示、类型约束的功能,并且在其他类型的模块中具有类型提示的功能。
目标
本文的目标是使用typescript实现两个函数:
- shuffle:打乱数组
- extend: 合并对象属性
之后打包这两个函数(打包的内容仅有这两个函数),创建typescript类型声明文件,发布到npm上。在新的工程中,typescript运行环境下能够直接使用这个包,并且具有类型提示以及类型约束的功能,es6以及node运行环境下具有类型提示的功能。在html中直接引入打包的js,也能够在浏览器中直接使用;
demo-ts-lib工程地址:https://gitee.com/martsforever-demo/demo-ts-lib
test-demo-ts-lib工程地址:https://gitee.com/martsforever-demo/test-demo-ts-lib
步骤
创建工程
- 创建工程
选择手动选择功能配置vue create demo-ts-lib
选择Babel、Typescript、Unit TestingMamually select features
接下来的除了测试选择Jest
,其他的选择默认就好了; - 初始化工程
- 安装依赖
npm i
或者cnpm i
- 启动项目,检查有没有问题:
npm run serve
- 检查没有问题之后,开始实现功能,并且创建类型声明文件;
- 安装依赖
结构调整
为了能够打包自定义的typescript包,这里需要将包源码与实例页面代码分开。
- 在根目录下创建一个home文件夹,然后将src目录下现有的文件全部移动到home文件夹下,src目录作为包的根目录,其中只存放函数相关的文件,而home则存放示例页面相关的文件;
- 根目录下的tsconfig.json中的include增加home目录扫描正则表达式:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", "types": [ "webpack-env", "jest" ], "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "home/**/*.ts", "home/**/*.tsx", "home/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules" ] }
源码实现
src/index.ts
import {DemoTsLibType, DemoTsModule} from "@/types";
const extend: DemoTsModule.extend = <T, U>(to: T, from: U) => {
const ret = {} as any
for (const key in to) {
ret[key] = to[key] as any
}
for (const key in from) {
ret[key] = from[key] as any
}
return ret as T & U
}
const shuffle: DemoTsModule.shuffle = <T>(array: T[]) => {
if (!array) return array
array = [...array]
let currentIndex = array.length;
let temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array
}
const DemoTsLib: DemoTsLibType = {
extend,
shuffle
}
export default DemoTsLib
src/types/index.d.ts
export namespace DemoTsModule {
type extend = <T, U>(to: T, from: U) => T & U
type shuffle = <T>(array: T[]) => T[]
}
export interface DemoTsLibType {
extend: DemoTsModule.extend
shuffle: DemoTsModule.shuffle
}
declare const DemoTsLib: DemoTsLibType;
export default DemoTsLib
这里提示两点:
- 第一个是在类型声明文件
index.d.ts
中声明类型,然后在index.ts
中使用根据类型实现,尽量避免类型声明与代码实现不一致的情况出现; - 第二点是,index.d.ts中的默认导出是一个值,不能是类型,而且类型声明文件
index.d.ts
中的默认导出也是一个值,这个值的类型要与index.d.ts
中默认导出的值的类型要一致。否则在新项目中无法得到正确的类型提示以及类型约束;如果index.d.ts
中默认导出的是一个类型,那么在typescript将无法正常使用;
在 home/App.vue中使用定义的DemoTsLib
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
</template>
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';
import DemoTsLib from "@/index";
@Component({
components: {
HelloWorld,
},
})
export default class App extends Vue {
mounted() {
const a = {name: '123'}
const b = {age: 20}
const c = DemoTsLib.extend(a, b)
console.log(c)
const arr = [1, 2, 3, 4, 5]
console.log(DemoTsLib.shuffle(arr))
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
可以看到,变量c能够得到正确的合并类型的提示:
启动以及打包配置
/build/config.doc.js
这个文件的作用是配置调试home文件夹中的单页面,在这个单页面中可以直接使用以及测试编写的DemoTsLib
const path = require('path')
const resolve = (dir) => path.join(__dirname, '../', dir)
console.log('run doc')
module.exports = {
publicPath: './',
devServer: {port: '8887'},
outputDir: resolve('docs'),
pages: {
index: {
entry: resolve('home/main.ts'),
template: 'public/index.html',
filename: 'index.html',
title: 'Index Page',
chunks: ['chunk-vendors', 'chunk-common', 'index']
},
},
chainWebpack: config => {
config.plugins.delete('prefetch-index')
},
}
/build/config.lib.js
这个文件的作用是配置打包DemoTsLib,打包不包括home文件夹下的示例页面,仅仅是index.ts中实现的两个函数,打包目标为umd格式,兼容浏览器,node以及es6模块规范;
const path = require('path')
const resolve = (dir) => path.join(__dirname, '../', dir)
console.log('run lib')
module.exports = {
outputDir: resolve('dist'),
configureWebpack: {
entry: {
'demo-ts-lib': resolve('src/index.ts')
},
output: {
filename: `[name].js`,
libraryTarget: 'umd',
libraryExport: 'default',
library: 'DemoTsLib',
globalObject: 'this'
},
},
css: {
extract: {
filename: `[name].css`
}
},
chainWebpack: config => {
config.optimization.delete('splitChunks')
config.plugins.delete('copy')
config.plugins.delete('preload')
config.plugins.delete('prefetch')
config.plugins.delete('html')
config.plugins.delete('hmr')
config.entryPoints.delete('app')
}
}
/build/index.js
module.exports = process.env.NODE_ENV === 'production' ? require('./config.lib') : require('./config.doc')
/vue.config.js
module.exports = require('./build/index')
/demo.html
这个文件的作用是测试在html文件中直接引入打包好的js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="./dist/demo-ts-lib.js"></script>
<script>
var a = {name: '123'}
var b = {age: 20}
var c = DemoTsLib.extend(a, b)
console.log(c)
var arr = [1, 2, 3, 4, 5]
console.log(DemoTsLib.shuffle(arr))
</script>
</body>
</html>
启动本地调试
npm run serve
浏览器控制台结果:
打包
npm run build
将会在根目录下生成dist文件夹
直接打开demo.html
这个demo.html直接在ie浏览器也是能够打开的。兼容到ie10;
发布到npm
修改package.json
{
"name": "demo-ts-lib",
"version": "0.1.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit"
},
"main": "dist/demo-ts-lib.js",
"files": [
"src/types",
"dist"
],
"types": "src/types/index.d.ts",
"dependencies": {
"core-js": "^2.6.5",
"vue": "^2.6.10",
"vue-class-component": "^7.0.2",
"vue-property-decorator": "^8.1.0"
},
"devDependencies": {
"@types/jest": "^23.1.4",
"@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-typescript": "^3.1.1",
"@vue/cli-plugin-unit-jest": "^3.1.1",
"@vue/cli-service": "^3.1.1",
"@vue/test-utils": "1.0.0-beta.29",
"babel-core": "7.0.0-bridge.0",
"ts-jest": "^23.0.0",
"typescript": "^3.4.3",
"vue-template-compiler": "^2.6.10"
}
}
没有npm账号的同学自行注册;注册完之后使用npm login 登录,然后发布:
npm publish
第一个版本0.1.0
全局安装typescript:
npm i -g typescript
创建一个空的文件夹:test-demo-ts-lib
进入该文件夹,执行以下命令初始化:
npm init
tsc --init
npm i
npm i tslib -D
全部回车,使用默认设置
使用ide打开目录,创建文件:src/helloworld.ts
package.json的启动脚本中增加两个启动脚本:
"build": "tsc",
"start": "tsc --watch"
安装 demo-ts-lib
npm i demo-ts-lib -S
在helloworld.ts中使用DemoTsLib:
src/helloworld.ts
import DemoTsLib from "demo-ts-lib";
const a = {name: '123'}
const b = {age: 456}
const c = DemoTsLib.extend(a, b)
console.dir(c)
const arr = [1, 2, 3, 4, 5]
console.log(DemoTsLib.shuffle(arr))
可以看到,c具有正确的类型提示功能:
执行 npm start
开启编译
会在helloworld.ts旁生成一个helloworld.js
使用node执行这个helloworld.js
node ./src/helloworld.js
结果:
根目录下创建test.js
const DemoTsLib = require('demo-ts-lib')
const a = {name: '123'}
const b = {age: 456}
const c = DemoTsLib.extend(a, b)
console.dir(c)
const arr = [1, 2, 3, 4, 5]
console.log(DemoTsLib.shuffle(arr))
可以看到,仍然具有正确的类型提示功能:
使用node执行这个test.js