- 小程序状态管理方案
详细的文档 [图片] [图片] redux 小程序适配方案。在小程序开发中使用 [代码]redux[代码] 管理全局状态。 尽管小程序入门门槛非常之低,但是在项目不停的迭代过程中,不可避免的项目代码复杂度也会越来越高,从前我们可以将跨页面数据管理在Storage或者SessionStorage中,利用一定的代码规范来管理不同页面不同开发者的数据,但随着时间的推移这种方式会造成代码、数据过于分散,且容易出错覆盖。再者每个页面间都需要手动的去 [代码]storage[代码] 读取数据,略显繁琐。 当项目开始变得复杂,我们想要统一的管理起状态数据,自动的同步、分发数据到需要的页面、组件([代码]Reactive[代码])。 安装 [代码]# 使用npm安装 npm i -S @wxa/redux # 使用yarn安装 yarn add @wxa/redux [代码] 基本用法 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中挂载插件 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { reducers: combineReducers(...your reducer), middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 注册完 redux 插件之后,将会自动的调用 [代码]redux.createStore[代码] 创建一个用于存储全局状态数据 [代码]store[代码],并且插件会在自动的挂载 [代码]store[代码] 到 App、Component、Page 实例中 [代码]$store[代码] 。 通过 [代码]this.$store.getState()[代码]可以获得所有全局状态。 通过 [代码]this.$store.dispatch()[代码]可以提交一个状态修改的 action。 更详细的 store api 获取全局状态 在页面/组件类中定义 [代码]mapState[代码] 对象,指定关联的全局状态(在[代码]react[代码]中叫[代码]connect[代码])。 [代码]import {Page} from '@wxa/core'; @Page export default class Index { mapState = { todolist$ : (state)=>state.todo, userInfo$ : (state)=>state.userInfo } add() { // dispatch change state. // todo list will auto add one. this.$store.dispatch({type: 'Add_todo_list', payload: 'coding today'}); } } [代码] 然后再[代码]template[代码]中就可以直接使用映射的数据了。 [代码]<view>{{userInfo$.name}}</view> <view wx:for="{{todolist$}}">{{key+1}}{{item}}</view> [代码] 得益于 [代码]@wxa/core[代码] 的 diff方法,redux在同步数据的时候只会增量的修改数据,而不是全量覆盖 😁 在任意位置获取全局状态数据 编写一些通用的基础函数提供给页面调用的时候,可能会需要从 [代码]store[代码] 中读取相应数据做处理。 例如在我们需要在所有请求的 postdata 中统一的加上用户的基本信息,可以这么实现: [代码]// 任意 api.js import {fetch} from '@wxa/core'; import {getStore} from '@wxa/redux'; export default const customFetch = (...args) => { let {idNo, name} = getStore().getState().UserModel; // 每个请求自动添加用户 args[1] = { idNo, name ...args[1], }; return fetch(...args); } [代码] 个性化页面数据 有时我们可能需要临时改写一下数据用于展示,实现类似 [代码]vue[代码] [代码]computed[代码] 的效果,此时我们可以相应的改造 mapState。 [代码]export default class A { mapState = { userInfo$(state){ let model = state.UserModel; // 自动掩码用户的身份证、姓名 // diff 数据并自动调用 setData this.$diff({ idNoCover: model.idNo.replace(/([\d]{4})(\d{10})([\dxX]{4})/, '$1***$3') }) return model } } } [代码] 分包用法 当小程序应用开始使用分包技术的时候,redux 方案也需要相应的做出优化,分包有以下特点: 引用原则 packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件 packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源 即当分包 A 定义了自己业务逻辑的数据 model 之后,且该 model 无法被其他分包复用,则我们完全可以把对应 model 放到分包的页面中,懒加载对应 [代码]redux.reducer[代码],以此减少主包体积。 为了做到懒加载对应的 reducer,我们需要在改造一下我们的代码。 挂载插件 在[代码]app.js[代码]/[代码]app.wxa[代码]中,改造 [代码]reducer[代码] 的注册方式。 [代码]// app.js or app.wxa import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: [promiseMiddleware] }) @App export default class Main {}; [代码] 动态添加分包 Reducer 假设我们在分包 A 中定义了专门用于订单处理的 [代码]reducer[代码],分包入口页面为 [代码]subpages/A/pages/board[代码]。 在分包页面被使用之前我们需要动态的注册一个新的 [代码]reducer[代码]。 [代码]// subpages/A/pages/board import {reducerRegistry} from '@wxa/redux'; import AOrderModel from '/subpages/A/models/order.model.js'; // 注册对应的数据 model reducerRegistry.register('AOrderModel', AOrderModel); [代码] 注册完毕之后,后续所有分包 A 的页面都可以正常的使用 [代码]mapState[代码] 中映射页面需要使用的状态数据。 调试 Redux [代码]@wxa/redux[代码] 提供了小程序 [代码]redux-remote-devtools[代码] 的适配代码。稍微改造一下我们的挂载插件部分的代码即可使用: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import { composeWithDevTools } from '@wxa/redux/libs/remote-redux-devtools.js'; import promiseMiddleware from 'redux-promise'; const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 打开开发者工具不校验合法域名开关,就可以正常使用 [代码]redux-devtools[代码] 了。 由于 [代码]devtools[代码] 仅用于开发阶段,我们可以利用 [代码]wxa[代码] 提供的依赖分析能力,按需引入。 改写上续配置如下: [代码]import {App, wxa} from '@wxa/core'; // 引入插件方法 import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux' import promiseMiddleware from 'redux-promise'; let composeEnhancers = (m) => m; if (process.env.NODE_ENV === 'production') { let composeWithDevTools = require('@wxa/redux/libs/remote-redux-devtools.js').composeWithDevTools; composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 }); } // 注册插件 wxa.use(wxaRedux, { // reducers: combineReducers(...your reducer), reducers: { UserModel: userReducer, AppModel: appReducer, ...your reducer }, middlewares: composeEnhancers(applyMiddleware(promiseMiddleware)) }) [代码] 如上配置,当 [代码]process.env.NODE_ENV[代码] 设置为生产环境的时候,[代码]@wxa/redux/libs/remote-redux-devtools.js[代码] 将不会被打包进 [代码]dist[代码] 持久化数据 某些场景,为了用户体验,我们需要将对应数据缓存下来,方便下次用户可以直接看到对应页面,此时我们需要将 [代码]store[代码] 的数据缓存下来,这里我们使用 [代码]redux-persist[代码] 用于持久化数据。 示例如下: [代码]import {wxa, App} from '@wxa/core'; import wxaRedux from '@wxa/redux'; import wxPersistStorage from '@wxa/redux/libs/wx.storage.min.js'; import {persistStore, persistReducer} from 'redux-persist'; import orderModel from './order.model.js'; let persistOrderModel = persistReducer({ key: 'orderModel', storage: wxPersistStorage, timeout: null, // 超时时间,设置为 null }, orderModel); wxa.use(wxaRedux, { reducers: { orderModel: persistOrderModel } }) @App export default class { onLaunch() { // 冷启动开始就加载缓存数据 persistStore(this.$store, {}, ()=>this.$storeReady=true); } } [代码] 实时日志 我们可以结合小程序实时日志和 [代码]redux-logger[代码] 一起使用。 [代码]import {wxa, App} from '@wxa/core'; import {createLogger} from 'redux-logger'; import wxaRedux from '@wxa/redux'; let log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : console; let logger = createLogger({ logger: log }); wxa.use(wxaRedux, { reducers: {...your reducers}, middlewares: [logger] }); [代码] 配置完毕之后,项目中所有的 [代码]Action[代码] 日志都将上报到微信的实时日志后台,开发者可以登录 mp.weixin.qq.com 查看用户所有操作记录。 配置 reducers 类型: Function [代码]combineReducers(...reducer)[代码]的返回 Object [代码]reducer[代码] 列表,用于动态注册场景 middlewares 类型: Array redux 中间件列表 Function [代码]applyMiddleware(...middlewares)[代码]的返回 initialState 类型: any reducer 初始状态,参考 [代码]redux 文档[代码] debug 类型: Boolean [代码]false[代码] 是否打印插件日志 技术细节 [代码]wxa/redux[代码]根据不同的实例类型有不同的任务,在App层,我们需要创建一个[代码]store[代码]并挂载到app中,在[代码]Page[代码]和[代码]Component[代码]层,我们做了更多细节处理。 App Level 创建[代码]store[代码],应用redux的中间件,挂载[代码]store[代码]到App实例。 Page Level 在不同的生命周期函数,有不同的处理。 [代码]onLoad[代码] 根据[代码]mapState[代码]订阅[代码]store[代码]的数据,同时挂载一个[代码]unsubscribe[代码]方法到实例。 [代码]onShow[代码] 标记页面实例[代码]$$isCurrentPage[代码]为[代码]true[代码], 同时做一次状态同步。因为有可能状态在其他页面做了改变。 [代码]onHide[代码] 重置[代码]$$isCurrentPage[代码],这样子页面数据就不会自动刷新了。 [代码]onUnload[代码] 调用[代码]$unsubscribe[代码]取消订阅状态 Component Level 针对组件生命周期做一些单独处理 [代码]created[代码] 挂载[代码]store[代码] [代码]attached[代码] 订阅状态,并同步状态到组件。 [代码]detached[代码] 取消订阅
2020-07-05 - 基于小程序请求接口 wx.request 封装的类 axios 请求
Introduction wx.request 的配置、axios 的调用方式 ----------------源码戳我--------------- ---------------demo 戳我-------------- feature 支持 wx.request 所有配置项 支持 axios 调用方式 支持 自定义 baseUrl 支持 自定义响应状态码对应 resolve 或 reject 状态 支持 对响应(resolve/reject)分别做统一的额外处理 支持 转换请求数据和响应数据 支持 请求缓存(内存或本地缓存),可设置缓存标记、过期时间 use app.js @onLaunch [代码] import axios form 'axios' axios.creat({ header: { content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, baseUrl: 'https://api.baseurl.com', ... }); [代码] page.js [代码]axios .post("/url", { id: 123 }) .then((res) => { console.log(response); }) .catch((err) => { console.log(err); }); [代码] API [代码] axios(config) - 默认get axios(url[, config]) - 默认get axios.get(url[, config]) axios.post(url[, data[, config]]) axios.cache(url[, data[, config]]) - 缓存请求(内存) axios.cache.storage(url[, data[, config]]) - 缓存请求(内存 & local storage) axios.creat(config) - 初始化定制配置,覆盖默认配置 [代码] config 默认配置项说明 [代码]export default { // 请求接口地址 url: undefined, // 请求的参数 data: {}, // 请求的 header header: "application/json", // 超时时间,单位为毫秒 timeout: undefined, // HTTP 请求方法 method: "GET", // 返回的数据格式 dataType: "json", // 响应的数据类型 responseType: "text", // 开启 http2 enableHttp2: false, // 开启 quic enableQuic: false, // 开启 cache enableCache: false, /** 以上为wx.request的可配置项,参考 https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html */ /** 以下为wx.request没有的新增配置项 */ // {String} baseURL` 将自动加在 `url` 前面,可以通过设置一个 `baseURL` 便于传递相对 URL baseUrl: "", // {Function} (同axios的validateStatus)定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 reject validateStatus: undefined, // {Function} 请求参数包裹(类似axios的transformRequest),通过它可统一补充请求参数需要的额外信息(appInfo/pageInfo/场景值...),需return data transformRequest: undefined, // {Function} resolve状态下响应数据包裹(类似axios的transformResponse),通过它可统一处理响应数据,需return res transformResponse: undefined, // {Function} resolve状态包裹,通过它可做接口resolve状态的统一处理 resolveWrap: undefined, // {Function} reject状态包裹,通过它可做接口reject状态的统一处理 rejectWrap: undefined, // {Boolean} _config.useCache 是否开启缓存 useCache: false, // {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 cacheName: undefined, // {Boolean} _config.cacheStorage 是否开启本地缓存 cacheStorage: false, // {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 cacheLabel: undefined, // {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 cacheExpireTime: undefined, }; [代码] 实现 axios.js [代码]import Axios from "./axios.class.js"; // 创建axios实例 const axiosInstance = new Axios(); // 获取基础请求axios const { axios } = axiosInstance; // 将实例的方法bind到基础请求axios上,达到支持请求别名的目的 axios.creat = axiosInstance.creat.bind(axiosInstance); axios.get = axiosInstance.get.bind(axiosInstance); axios.post = axiosInstance.post.bind(axiosInstance); axios.cache = axiosInstance.cache.bind(axiosInstance); axios.cache.storage = axiosInstance.storage.bind(axiosInstance); [代码] Axios class 初始化 defaultConfig 默认配置,即 defaults.js axios.creat 用户配置覆盖默认配置 注意配置初始化后 mergeConfig 不能被污染,config 需通过参数传递 [代码]constructor(config = defaults) { this.defaultConfig = config; } creat(_config = {}) { this.defaultConfig = mergeConfig(this.defaultConfig, _config); } [代码] 请求别名 axios 兼容 axios(config) 或 axios(url[, config]); 别名都只是 config 合并,最终都通过 axios.requst()发起请求; [代码] axios($1 = {}, $2 = {}) { let config = $1; // 兼容axios(url[, config])方式 if (typeof $1 === 'string') { config = $2; config.url = $1; } return this.request(config); } post(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', }; return this.request(config); } [代码] 请求方法 _request 请求配置预处理 实现 baseUrl 实现 transformRequest(转换请求数据) [代码] _request(_config = {}) { let config = mergeConfig(this.defaultConfig, _config); const { baseUrl, url, header, data = {}, transformRequest } = config; const computedConfig = { header: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', ...header, }, ...(baseUrl && { url: combineUrl(url, baseUrl), }), ...(transformRequest && typeof transformRequest === 'function' && { data: transformRequest(data), }), }; config = mergeConfig(config, computedConfig); return wxRequest(config); } [代码] wx.request 发起请求、处理响应 实现 validateStatus(状态码映射 resolve) 实现 transformResponse(转换响应数据) 实现 resolveWrap、rejectWrap(响应状态处理) [代码]export default function wxRequest(config) { return new Promise((resolve, reject) => { wx.request({ ...config, success(res) { const { resolveWrap, rejectWrap, transformResponse, validateStatus, } = config; if ((validateStatus && validateStatus(res)) || ifSuccess(res)) { const _resolve = resolveWrap ? resolveWrap(res) : res; return resolve( transformResponse ? transformResponse(_resolve) : _resolve ); } return reject(rejectWrap ? rejectWrap(res) : res); }, fail(res) { const { rejectWrap } = config; reject(rejectWrap ? rejectWrap(res) : res); }, }); }); } [代码] 请求缓存的实现 默认使用内存缓存,可配置使用 localStorage 封装了 Storage 与 Buffer 类,与 Map 接口一致:get/set/delete 支持缓存标记&过期时间 缓存唯一 key 值,默认使用 url&data 生成,无需指定 [代码] import Buffer from '../utils/cache/Buffer'; import Storage from '../utils/cache/Storage'; import StorageMap from '../utils/cache/StorageMap'; /** * 请求缓存api,缓存于本地缓存中 */ storage(url, data = {}, _config = {}) { const config = { ..._config, url, data, method: 'POST', cacheStorage: true, }; return this._cache(config); } /** * 请求缓存 * @param {Object} _config 配置 * @param {Boolean} _config.useCache 是否开启缓存 * @param {String} _config.cacheName 缓存唯一key值,默认使用url&data生成 * @param {Boolean} _config.cacheStorage 是否开启本地缓存 * @param {Any} _config.cacheLabel 缓存标志,请求前会对比该标志是否变化来决定是否使用缓存,可用useCache替代 * @param {Number} _config.cacheExpireTime 缓存时长,计算缓存过期时间,单位-秒 */ _cache(_config) { const { url = '', data = {}, useCache = true, cacheName: _cacheName, cacheStorage, cacheLabel, cacheExpireTime, } = _config; const computedCacheName = _cacheName || `${url}#${JSON.stringify(data)}`; const cacheName = StorageMap.getCacheName(computedCacheName); // return buffer if (useCache && Buffer.has(cacheName, cacheLabel)) { return Buffer.get(cacheName); } // return storage if (useCache && cacheStorage) { if (Storage.has(cacheName, cacheLabel)) { const data = Storage.get(cacheName); // storage => buffer Buffer.set( cacheName, Promise.resolve(data), cacheExpireTime, cacheLabel ); return Promise.resolve(data); } } const curPromise = new Promise((resolve, reject) => { const handleFunc = (res) => { // do storage if (useCache && cacheStorage) { Storage.set(cacheName, res, cacheExpireTime, cacheLabel); } return res; }; this._request(_config) .then((res) => { resolve(handleFunc(res)); }) .catch(reject); }); // do buffer Buffer.set(cacheName, curPromise, cacheExpireTime, cacheLabel); return curPromise; } [代码]
2020-07-03 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27