- Vue项目使用kbone打包成小程序的踩坑笔记
以下内容是初版kbone的笔记,现在kbone进行了大量的更新升级,以下配置方法大部分已经不适用新版kbone了,如需详细了解kbone的使用方法,请参考官方文档 kbone是大佬 @June 开发的一款Vue项目打包成小程序的工具(暂时只支持微信小程序) 已有的Vue项目只需通过简单的配置即可打包成小程序的代码,新项目也可以使用这个工具进行同步开发,项目中提供了很多示例供参考 Github连接:https://github.com/wechat-miniprogram/kbone 为什么选择kbone? 市面上有很多跨平台的框架,如: WePY、mpvue、uni-app、taro等 他们都有一些弊端和限制,现有的项目迁移不方便,对第三方组件库不兼容等 kbone作为一款工具,也可以说是一款webpack插件(Vue项目只需要引入 mp-webpack-plugin 插件,通过简单的webpack配置即可),无论是对新项目还是已有项目都是非常友好的,因为他完整的移植了Vue Runtime,通过webpack将Vue代码打包成小程序兼容的代码,通过适配器运行,所以大部分的第三方组件库是兼容的 如何使用? Github仓库中提供了很多示例,有兴趣的可以体验下,下面介绍下我的使用过程 创建项目 因为习惯了使用Vue Cli,所以我选择使用这个工具初始化一个项目,使用的版本是Vue Cli 3(这个工具提供了一套完全图形化的用户界面,这里就不介绍使用方法了,没有使用过的同学可以 点击这里 查看创建方法) 集成插件 kbone目前还没有提供Vue Cli的插件(大佬很忙,有能力的同学可以在Github上提PR),所以需要手动添加,具体的操作方式请参考QuickStart文档 安装 mp-webpack-plugin 插件 [代码]yarn add mp-webpack-plugin --dev # 或者 npm install mp-webpack-plugin --save-dev [代码] 在 src 目录中新增 main.mp.js 入口文件 [代码]import Vue from 'vue' import App from '@/App' import router from '@/router' import store from '@/store' // 需要将创建根组件实例的逻辑封装成方法 export default function createApp () { // 在小程序中如果要注入到 id 为 app 的 dom 节点上,需要主动创建 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) Vue.config.productionTip = false return new Vue({ router, store, render: h => h(App) }).$mount('#app') } [代码] 在根目录创建 miniprogram.config.js 文件,添加 mp-webpack-plugin 插件配置 [代码]module.exports = { // 页面 origin,默认是 https://miniprogram.default origin: '', // 填写项目中的图片资源地址,建议图片资源使用线上地址 // 入口页面路由,默认是 / entry: '/', // 页面路由,用于页面间跳转 router: { // 路由可以是多个值,支持动态路由 index: [] }, // 特殊路由跳转 redirect: { // 跳转遇到同一个 origin 但是不在 router 里的页面时处理方式,支持的值:webview - 使用 web-view 组件打开;error - 抛出异常;none - 默认值;什么都不做,router 配置项中的 key notFound: 'index', // 跳转到 origin 之外的页面时处理方式,值同 notFound accessDenied: 'index' }, // app 配置,同 https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#window app: { navigationStyle: 'custom' // 自定义navigation }, // 全局配置 global: {}, // 页面配置,可以为单个页面做个性化处理,覆盖全局配置 pages: {}, // 优化 optimization: { domSubTreeLevel: 5, // 将多少层级的 dom 子树作为一个自定义组件渲染,支持 1 - 5,默认值为 5 // 对象复用,当页面被关闭时会回收对象,但是如果有地方保留有对象引用的话,注意要关闭此项,否则可能出问题 elementMultiplexing: true, // element 节点复用 textMultiplexing: true, // 文本节点复用 commentMultiplexing: true, // 注释节点复用 domExtendMultiplexing: true, // 节点相关对象复用,如 style、classList 对象等 styleValueReduce: 5000, // 如果设置 style 属性时存在某个属性的值超过一定值,则进行删减 attrValueReduce: 5000 // 如果设置 dom 属性时存在某个属性的值超过一定值,则进行删减 }, // 项目配置,会被合并到 project.config.json projectConfig: { appid: '', // 填写小程序的AppId projectname: '' // 填写小程序的项目名称 }, // 包配置,会被合并到 package.json packageConfig: { name: '', // 项目名称 description: '', // 描述 author: '' // 作者信息 } } [代码] 在根目录创建 .env.mp 文件,添加 mp 环境变量 [代码]NODE_ENV = mp [代码] 修改 vue.config.js 文件,添加打包小程序的 webpack 配置 [代码]const path = require('path') function resolve (dir) { return path.join(__dirname, dir) } const webpack = require('webpack') const MpWebpackPlugin = require('mp-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { css: { extract: true }, outputDir: process.env.NODE_ENV === 'mp' ? './dist/mp/common' : './dist/web', configureWebpack: { resolve: { extensions: ['*', '.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') } } }, chainWebpack: config => { if (process.env.NODE_ENV === 'mp') { config .devtool('node') .entry('app').clear().add('./src/main.mp.js').end() .output.filename('[name].js') .library('createApp') .libraryExport('default') .libraryTarget('window').end() .target('web') .optimization.runtimeChunk(false) .splitChunks({ chunks: 'all', minSize: 1000, maxSize: 0, minChunks: 1, maxAsyncRequests: 100, maxInitialRequests: 100, automaticNameDelimiter: '~', name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } }).end() .plugins.delete('copy').end() .plugin('define').use(new webpack.DefinePlugin({ 'process.env.isMiniprogram': process.env.isMiniprogram // 注入环境变量,用于业务代码判断 })).end() .plugin('extract-css').use(new MiniCssExtractPlugin({ filename: '[name].wxss', chunkFilename: '[name].wxss' })).end() .plugin('mp-webpack').use(new MpWebpackPlugin(require('./miniprogram.config.js'))).end() } } } [代码] 修改 package.json 中的 scripts 属性,添加用于开发和打包的任务 [代码]# 开发 "mp-serve": "vue-cli-service build --watch --mode mp" # 打包 "mp-build": "vue-cli-service build --mode mp" [代码] 配置完成,尽情玩耍吧! 目前已知的问题 小程序中无法支持 getComputedStyle 和 getBoundingClientRect 的同步接口,只能异步,所以依赖这两个方法的第三方库,需要进行改造 因为大佬很忙目前还处于开发阶段,很多组件还没有适配,有能力和有精力的同学欢迎去Github提PR 再次奉上Github连接:https://github.com/wechat-miniprogram/kbone
2019-12-10 - 自定义组件扩展能力不能满足的需求:单一表现形式下的多变处理逻辑?
现有的项目里有一个cell插件 业务上有一需求:一个列表的cells们 有title,time,headimg等固定的表现元素,且样式也是不变的,但是绑定的data[list][cell]数据来源由接口[href,href,href]们中的一个获得. 症结所在是: 每个href接口不能保证返回的数据结构统一对应到cell的数据结构,需要对数据进行转换处理绑定到data[list][cell] 🤔 方案1:是把每一种接口的数据转换写在cell插件里 (好复杂的)❌ 方案2: 每一个接口创建一个组件 把公共逻辑放到 behaviors里 (三个插件就是12个文件 wxml,wxss代码还是相同的)❌ 方案3: 创建一个cell 根据传入的 properties 动态引入behaviors, 应该没有这个功能吧! 😂 //如果有请狠狠批评我,把使用方法甩我脸上 组件能否在像class一样 cell_href1 extends cell { behaviors } 这样由原有的组件直接扩展出一个新的组件,而且不用再去新建一堆js,ml,ss?
2019-08-09 - 小程序转支付宝小程序工具:wx2my
背景目前市面上有很多微信小程序,同时开发者开发完微信小程序后,希望可以同时发布到支付宝小程序平台上,可惜微信小程序并不能直接发布到支付宝平台上,两个平台小程序不兼容。因此开发者需要对微信小程序代码进行修改,调整成支付宝小程序代码。 庆幸的事两种小程序代码有很多相似之处,手动修改比较繁琐,因此小程序助手孕育而生。达到自动把微信小程序代码转换成支付宝小程序。不过由于两种小程序功能和api等的不一致,转换后生成的支付宝小程序并不能直接运行起来,还需要进行代码检查,手动的修改无法转换的部分。 地址 vscode插件: wx2my(微信小程序转支付宝小程序) cli命令工具: wx2my npm地址 使用文档: wx2my 语雀地址 目标 快速转换微信小程序为支付宝小程序,达到快速转换,降低转换成本,这样可以早点下班。 视频教程[视频] 能力 文件名转换app文件名转换: 微信小程序 --> wx2my --> 支付宝小程序 app.json app.json app.js app.js app.wxss app.acss page页面、自定义组件文件名转换: 微信小程序 --> wx2my --> 微信小程序 index.json index.json index.js index.js index.wxml index.axml index.wxss index.acss 其他类型文件名转换: 微信小程序 --> wx2my --> 支付宝小程序 parse.wxs parse.sjs 其他类型文件(图片、视频等) 直接复制,不转换 文件内容转换app.json 转换 app.json文件为整个小程序配置文件,不过微信小程序app.json和支付宝小程序在app.json配置文件支持的能力不完全一致,部分一致的但名称不一致的配置,转换工具会分析并转换出来。 转换方式: navigationBarTitleText --> defaultTitle enablePullDownRefresh --> pullRefresh navigationBarBackgroundColor --> titleBarColor ...等 其中微信小程序支持,支付宝小程序不支持的,需要开发者自己手动修改,如:networkTimeout、functionalPages、workers等 全局组件转换 微信小程序支持全局组件,即在app.json中添加usingComponents字段,这样在小程序内的页面或自定义组件中可以直接使用全局组件而无需再声明。 转换方式: 转换工具会分析小程序中所有页面和组件,找到那些使用了全局组件的页面和组件,并把全局组件声明在页面或组件的json文件中,当做普通组件引用和使用。同时把全局组件的声明删除。 wxml文件转换 转换逻辑是以wx:xxx开头的,替换为a:xxx方式。 a. 事件相关的转换,微信中 bindeventname 或 bind:eventname 转换为 onEventname, 如下: 转换前: <page bindtap="showName" bind:input = "actionName" catchchange="catchchange"bindtouchend="onTouchEnd"></page> 转换后: <page onTap="showName" onInput = "actionName" catchChange="catchchange" onTouchEnd="onTouchEnd"></page> b: 循环语句转换, 如下: 转换前: <view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName" wx:key="unique">{{idx}}: {{itemName.message}}</view> 转换后: <view a:for="{{array}}" a:for-index="idx" a:for-item="itemName" a:key="unique"> {{idx}}: {{itemName.message}}</view> c: wxs代码转换,微信小程序中的wxs功能对应支付宝小程序中的sjs功能,微信wxml中支持引用外部wxs文件和内联wxs代码,支付宝中只支持引用外部文件方式使用sjs,不支持内联sjs代码。 转换方式:转换工具分享所有wxml文件,找到wxs内联代码,提取wxs的内联代码,生成sjs文件,并使用外部引用的方式引入sjs文件,如下: 转换前: <wxs src="../wxs/utils.wxs" module="utils" /><wxs src="../wxs/utils.wxs" module="utils"> </wxs><wxs module="parse"> module.exports.getMax = getMax;</wxs> 转换后: <import-sjs from="../wxs/utils.sjs" name="utils" /><import-sjs from="../wxs/utils.sjs" name="utils"/><import-sjs from="./parse.sjs" name="sjsTest" />并在同级目录下创建了 [代码]parse.sjs[代码] 文件,并转换wxs的CommonJS为ESM parse.sjs文件内容: export default { getMax }; d: 无法替换完成的,在转换后的支付宝小程序的代码中,插入注释代码,提醒开发者并需要开发者手动检查修改。如下: 转换前: <cover-image class="img" src="/path/to/icon_play" bindload="bindload" binderror="binderror" aria-role="xxx"aria-label="xxx"/> 转换后: <cover-image class="img" src="/path/to/icon_play" bindload="bindload" binderror="binderror" aria-role="xxx"aria-label="xxx"/><!-- WX2MY: 属性bindload、binderror、aria-role、aria-label不被支持,请调整 --> 出现这种情况,开发者可以手动的搜索 [代码]WX2MY:[代码] 关键字,查找需要修改的代码 js文件转换 转换工具对api相关的调用转换使用了桥接文件 [代码]wx2my.js[代码] ,在所有js文件顶部引入wx2my.js文件,对api的调用,使用桥接函数,桥接函数对api参数不一致的地方在函数内部进行处理,如下: 转换前: wx.request(opts) 转换后: wx2my.request(opts) [代码]wx[代码] 转换为 [代码]wx2my[代码] ,其中wx2my为前进函数对外的方法 桥接函数中 [代码]request[代码] 的方法如下: [图片]
2019-04-17 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - 都 9102 年了,TypeScript 了解一下
前言 此文章为 SlugTeam 大前端技术沙龙 TypeScript 主题分享 PPT 文字内容。SlugTeam 大前端是腾讯互娱市场平台部营销技术中心下属几个开发组前端技术人员的联合,自去年 9 月起,SlugTeam 大前端每 3 个星期举行 1 次技术沙龙,沙龙主题由团队成员推荐并投票选出。 本次 3 月 28 日沙龙主题为 TypeScript。TypeScript 是由微软开发并开源的一门编程语言,其作为 Javascript 的超集被广大 Web 前端所熟知,即使在实际工作中尚未使用,也一定听说过 TypeScript。 接下来,让我们一起走近 TypeScript,了解这门越来越火的编程语言。 1. TypeScript 简介 1.1 JavaScript 的诞生 1995 年,网景公司发布了一门叫做 Javascript 的脚本语言,它由当时 34 岁的系统程序员 Brendan Eich 设计,仅仅用于在浏览器实现一些简单的网页互动,如表单验证。 Brendan Eich 做梦也没想到,自己花了十天仓促设计出来的 JavaScript,一经推出就被广泛接受,获得了全世界范围内大量的用户使用。JavaScript 这样连续的爆发式扩散增长,使得语言规格调整困难重重。 1996 年,微软公司也推出了自己的脚本 JScript。为了压制微软,网景公司决定申请 JavaScript 国际标准。 1997 年 6 月,第一个国际标准 ECMA-262 正式颁布。 从推出到颁布标准,JavaScript 仅用了一年半时间,语言的缺陷没有充分暴露,就已经被标准固化下来。 1.2 AJAX 的流行 在 2005 年以前,Web 应用程序开发还停留在完成简单的交互逻辑,受限于浏览器技术,Web 应用程序的交互行为十分单调,缺失像桌面应用那样丰富的交互,每次用户与服务器交互都需要重新刷新页面才能够看到效果。 2005 年初,Google 在其著名的 Web 交互应用程序中大量使用了 AJAX,如 Google、Google Map、Gmail等,无需刷新页面就能实现页面的异步更新。Google 对 AJAX 技术的实践,迅速提高了人们使用该项技术的意识。而使用了 AJAX 技术的 Web 应用程序,可以模拟出传统桌面应用的使用体验。 从此,Web 应用程序逐渐变得复杂而丰富,出现了越来越多的大型 Web 应用程序。 1.3 TypeScript 的由来 正如上面介绍的那样,JavaScript 设计之初就没考虑过可以用来编写大型应用。 JavaScript 诞生之初时的 Web 应用程序也就只有几十行或者几百行代码。到了 2010 年,Web 应用程序的代码已经达到了成千上万行。 2010 年的 JavaScript 标准为 ECMAScript 5.0 版本,尚未拥有类、模块等适合大型应用开发的概念。 与此同时 JavaScript 是动态脚本语言,这意味着没有静态类型,使得 IDE 无法提供诸如“代码补全”、“IDE 重构”(借助于 IDE 来对代码进行重构)、“属性查询”、“跳转到函数定义”等强大功能。 再加上 JavaScript 本身语言上的缺陷,使用 JavaScript 编写大型应用成了一件非常艰巨的任务。 微软卓越的工程师 Anders Hejlsberg(Delphi 和 C# 之父),留意到过去五年听到越来越多开发者吐槽 “JavaScript 难以编写大型应用程序”,使得他思考起未来 JavaScript 将何去何从。 Anders Hejlsberg 发现,不少开发者为了编写大型 Web 应用程序,会选择使用 GWT(Google Web Toolkit, 将 Java 代码编译成 JavaScript) 或者 Script# (将 C# 代码编译成 JavaScript),这样他们就可以借助 eclipse 这种强大的 IDE 辅助开发。 但是这种模式有着很大的缺点:在浏览器上面调试一堆由编译器生成的 JavaScript 是件很让人崩溃的事情。而且程序员写的是 Java,出来的是 JavaScript,这本身就是一件很违和的事情。 于是,Anders Hejlsberg 把心思放在了增强 JavaScript 能力上面,思考如何在提供诸如类、模块、可选的静态类型等概念的同时,又不牺牲 JavaScript 现有的优点。 2012 年 10 月,Anders Hejlsberg 带领团队开发出了首个公开版本的 TypeScript。 2 TypeScript 知识 按照官方的说法: (1) TypeScript 设计目标是开发大型应用 (2) TypeScript 是 JavaScript 的严格超集,任何现有的 JavaScript 程序都是合法的 TypeScript 程序,包括各种 JavaScript 库 (3) TypeScript 增加了静态类型、类、模块、接口和类型注解 接下来,我们将通过一些实际例子,来学习了解 TypeScript (非教程,若想深入学习请到官网)。 2.1静态类型批注 [代码]/** * @param (string) x */ function process(x){ x.name = 'foo'; var v = x + x; alert(v); } [代码] 以上是一段日常很常见的 JavaScript 代码,定义了一个名为 process 的函数,传入参数 x,并利用工具生成参数注释。 而在 TypeScript 中,我们可以选择给参数添加类型批注: [代码]function process(x:string){ x.name = 'foo'; var v = x + x; alert(v); } [代码] 加了类型批注后,编译时候就会启动类型检查:“name” 下面出现了红色的错误提示波浪线,将鼠标移动至上面,则提示 “Property ‘name’ does not exits on type ‘string’.”。 继续将鼠标移动到第二行的 “v” 上面,提示 “(local var) v:string”,TypeScript 根据 x 的类型批注,很智能地推断出了 v 的类型。 [代码]function process(x: boolean){ var v = x + x } [代码] 我们试着把 x 标注为 boolean 类型,修改后 ‘x + x’ 下面会出现错误提示波浪线,提示 “Operator ‘+’ cannot be applied to types ‘boolean’ and ‘boolean’.”。 [代码]function process(x:string[]){ x[0]. } [代码] 我们试着将 x 类型批注改为字符串数组,输入 ‘x[0].’ 后,编译器会显示对应的智能提示。 该功能十分强大,它使得开发者不需要记忆过多的 api 或 property,直接就能在右侧找到目标对应的 api 或者 property。 开发者也无需担心这些 api 或 property 会出现张冠李戴的问题,能展示出来的方法或者属性在语法上都是正确的。 像 sublime text 之类的编辑器插件,虽然也能够提供代码补全,但是有可能补全的 api 或者 property 是错误的,因为它们的原理是根据输入的字符串做一些简单的匹配。 例如输入 ‘document’,接着输入’.g’,也即是 ‘document.g’,插件匹配到该字符串后,代码补全提示仅仅显示 ‘getElementsByTagName’ 方法。 这在 TypeScript 未出现之前,的确为开发提供了很大的便利。但是和 TypeScript 的代码补全对比起来,还是显得不够智能。 传统的参数注释,遇上结构化的对象参数,就很束手无策了。而在 TypeScript 中,我们可以标注任何类型的参数: [代码]interface Thing{ a: number; b: string; c?: boolean; } function process(x: Thing){ return x.c; } var n = process({a:10, b:"hello"}); [代码] 上面代码中,在参数后面添加 ‘?’,表示参数可选。 [代码]interface Thing{ a: number; b: string; foo(s: string): string; foo(n: number): number; } function process(x: Thing){ return x.foo("12"); //return x.foo(2); } [代码] 此外,TypeScript 还支持方法重载。 上面的 foo 方法也可以写成下面的格式: [代码]interface Thing{ a: number; b: string; foo: { (s: string): string; (n: number): number; data: any; }; } function process(x: Thing){ return x.foo.data; //return x.foo(2); } [代码] 对于函数,TypeScript 也能够批注其为某个接口的实现。 [代码]interface Accumulator { clear(): void; add(x:number): void; result(): number; } function makeAccumulator(): Accumulator{ var sum = 0; return { clear: function() {sum = 0}, addx: function(value: number){sum += value}, result: function(){return sum} } } var a = makeAccumulator(); a.add(5); [代码] makeAccumulator 里面的 ‘addx: function(value: number){sum += value}’ 将会被标红,因为 Accumulator 接口并没有定义 addx 函数。 [代码]window.onmousemove = function(e){return e.clientX}; var hash = location.hash; [代码] TypeScript 内部维护了一份 DOM/BOM 的方法属性,省去了用户自己添加对应批注的功夫。例如将鼠标移动到 ‘hash’ 上面,会提示 ‘var hash: string’,TypeScript 就能自动推断出类型。 TypeScript 最终将会编译成 JavaScript,在 TypeScript 上面定义的类型,编译后实际上是不存在的。 [代码]addingTypes.ts function Greeter(greeting: string) { this.greeting = greeting; } Greeter.prototype.greet = function() { return "Hello, " + this.greeting; } let greeter = new Greeter("world"); let button = document.createElement('button'); button.textContent = "Say Hello"; button.onclick = function() { alert(greeter.greet()); }; document.body.appendChild(button); [代码] 上面 TypeScript 文件编译为 JavaScript 后,将是下面的样子: [代码]addingTypes.js function Greeter(greeting) { this.greeting = greeting; } Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; var greeter = new Greeter("world"); var button = document.createElement('button'); button.textContent = "Say Hello"; button.onclick = function () { alert(greeter.greet()); }; document.body.appendChild(button); [代码] 2.2 类 [代码]class Point{ x: number; y: number; private color: string; constructor(x: number, y: number){ this.x = x; this.y = y; this.color = "red"; } dist(){ return Math.sqrt(this.x * this.x +this.y * this.y);} static origin = new Point(0,0); } var p = new Point(10, 20); p.x = 10; p.y = 20; //p.color = "green" [代码] 在 TypeScript 的类中,访问修饰符 public、private、protected、static 和 JAVA、C# 等语言类似。 [代码]class Point{ private color: string; constructor(public x: number = 0, public y: number = 0){ this.color = "red"; } dist(){return Math.sqrt(this.x * this.x + this.y * this.y)} static origin = new Point(0,0); } var p = new Point(); p.x = 10; p.y = 20; [代码] TypeScript 支持默认值,用法和 ES6 类似。 接下来,我们看一下 TypeScript 如何实现类的继承。 [代码]class Point{ private color: string; constructor(public x: number = 0, public y: number = 0){ this.color = "red"; } dist(){return Math.sqrt(this.x * this.x + this.y * this.y)} static origin = new Point(0,0); } class Point3D extends Point{ constructor(x: number, y: number, public z:number){ super(x,y); } dist() { var d = super.dist(); return Math.sqrt(d * d + this.z * this.z) } } [代码] 让我们继续看下一段代码 [代码]class Tracker{ count = 0; start(){ window.onmousemove = function(e){ this.count++; console.log(this.count); } } } var t = new Tracker(); t.start(); [代码] 这段代码在 ‘count++’ 处有个错误提示, “Property ‘count’ does not exist on type ‘GlobalEventHandlers’”,这也是新手在写 JavaScript 时候容易犯的一个错误,而且还不容易察觉。 我们可以使用箭头函数来修复这错误。 [代码]class Tracker{ count = 0; start(){ window.onmousemove = e => { this.count++; console.log(this.count); } } } var t = new Tracker(); t.start(); [代码] 2.3 模块 [代码]module Utils{ export class Tracker{ count = 0; start(){ window.onmousemove = e => { console.log(this.count) } } } } module Utils { export var greeting = "hello" } var t = new Utils.Tracker(); t.start; [代码] 假如模块带有很长的命名空间,如下面的代码 [代码]module Acme.Core.Utils{ export class Tracker{ count = 0; start(){ window.onmousemove = e => { console.log(this.count) } } } } var t = new Acme.Core.Utils.Tracker(); t.start; [代码] 我们可以 import 的形式来缩短命名空间。 [代码]module Acme.Core.Utils{ export class Tracker{ count = 0; start(){ window.onmousemove = e => { console.log(this.count) } } } } import ACM = Acme.Core.Utils; var t = new ACM.Tracker(); t.start; [代码] 假如我们要使用诸如 Nodejs 的模块,我们要先安装对应的 @types 文件,基本上流行的库都有社区维护的 type 文件。 网站 [代码]https://microsoft.github.io/TypeSearch/[代码] 可以查询相关库的 type 文件。 接下来拿 Nodejs 做个演示。 [代码]yarn global add typescript cd typescript_demo tsc --init yarn add @types/node [代码] 在目录打开 [代码]tsconfig.json[代码] 文件,修改对应字段 [代码]"typeRoots": [ "./node_modules/@types" ], "esModuleInterop": true [代码] 创建 server.ts 和 hello.ts,代码分别如下 [代码]server.ts import * as http from 'http' //import http from 'http' //上面写法会报“TS1192: Module '"http"' has no default export” export function simpleServer(port: number, message: string){ http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/html'}); res.write(`<h1>${message}</h1>`); res.end() }).listen(port) } [代码] [代码]hello.ts import {simpleServer} from './server'; simpleServer(1337, "Greetings Channel 9"); console.log("Listening..."); [代码] 保存后,编译 hello.ts 并运行 hello.js。 [代码]tsc hello.ts node hello [代码] 在浏览器访问 [代码]localhost:1337[代码] 可看到页面输出 [代码]Greetings Channel 9[代码]。 2.4 Visual Studio Code 与 TypeScript Visual Studio Code 是前端界最流行的 IDE,由微软开发并开源。 Visual Studio Code 加上 TypeScript,代码重构将变得很轻松。 我们在进行代码重构的时候,常常会遇上这么一种情况:修改了方法名或者属性名,还得找到引用它们的地方,逐个修改。假如引用的地方很多,修改将很耗费时间,也很繁琐,而且还可能修改得不够彻底,造成程序错误。 有了 TypeScript 后,在 Visual Studio Code 上,只需要在方法或者属性定义处,右键选择“重命名符号”,输入新的名字,回车后所有的引用将自动更新该变化。 2.5 TypeScript 与热门前端框架 前端目前流行三大框架: Angular、React、Vue。 其中 Angular 基于 TypeScript 来开发,React 在开发的时候可以选择引入 TypeScript,而对于 Vue,作者是这么说的: 必须要承认的是,2.x 的 TS 支持显然跟 React 和 Angular 是有差距的,这也是为什么 3.0 要加强这一块 这里推荐看一下尤雨溪在知乎上面的回答:TypeScript 不适合在 vue 业务开发中使用吗? 虽然目前 Vue 对 TypeScript 的支持不是很完美,但是大部分 TypeScript 的功能还是可以用,应用到生产环境中是个不错的选择。 Vue CLI3 已经支持生成 TypeScript 项目,假如是使用 React 的话,可以选择用 create-react-app 创建项目。 3. TypeScript 相关 3.1 TypeScript 与 ESLint 在使用 TypeScript 的时候,一般也会加入 TSLint,TSLint 事实上已经是 TypeScript 项目的标准静态代码分析工具。TSLint 的生态由一个核心的规范集,社区维护的多种自定义规则以及配置包组成。 ESLint 是 JavaScript 的标准静态代码分析工具。相对于 TSLint,ESLint 支持 TSLint 所缺少的很多功能,如条件 lint 配置和自动缩进。 有一段时间,TypeScript 的代码检查主要有两个方案:使用 TSLint 或使用 ESLint + typescript-eslint-parser。 在 2019 年年初,由于效能问题, TypeScript 官方决定全面采用 ESLint。 接下来 ESLint 团队将不再继续维护 typescript-eslint-parser,他们会封存储存库,也不会在 Npm 发布 typescript-eslint-parser,原本使用 typescript-eslint-parser 的开发者应使用 typescript-eslint/ parser 替代 3.2 Deno “Node 现在太难用了!”,Nodejs之父 Ryan Dahl 去年年初要开发一款 JavaScript 数据互动分析工具的时候,忍不住抱怨自己十年前创造的技术。 尽管 Nodejs 大受欢迎,但是 Ryan Dahl 在 2018 年的演讲时,坦言 Nodejs 有十大设计错误。 Ryan Dahl 决定偿还当年的技术债,打造一个全新的服务端 JavaScript 运行环境,也就是 Deno 项目。 Deno 跟 Nodejs 一样采用了 V8 引擎,但 Deno 是以 TypeScript 为基础,提高了代码的准确性,并将编译器内置到 Deno 可执行文件中。 需要一提的是,Deno 项目现在属于飞速发展的阶段,源码随时可能更新。 4 总结 4.1 TypeScript 优点 解决了 IDE 无法智能提示的问题 函数文档化,无需看接口文档即可直观了解函数参数及对应类型 类型检查以及错误提示 放心地进行代码重构 提供了业界认可的类、泛型、封装、接口面向对象设计能力,以提升 JavaScript 的面向对象设计能力 4.2 TypeScript 缺点 npm 绝大多数模块没有类型注解,假如在 @types 里面也没有找到对应的类型定义文件 (*.d.ts),需要自己手写一份 额外的语法学习成本 TypeScript 配合 webpack 或者 babel 等工具时需要额外处理一些异常 5 后记 在沙龙最后的讨论阶段,SlugTeam 成员一致认为 TypeScript 代表了前端未来发展的方向,项目迁移成本不算高,可以逐步推广起来。 还没了解 TypeScript 的同学,强烈安利:)
2019-04-17