- 微信小程序使用自定义目录(文件路径)进行下载/保存 案例(fail permission denied 解决方案)
场景描述 最近项目中有一个需要把网络文件下载下来保存到本地,然后对下载的文件进行读取,待文件不再使用后把文件进行删除的需求。当然也类似的需求还有很多,比如把小程序中的临时图片/文件永久保存下来等等,都是对文件操作的典型场景。 常见问题 在以上场景的实现过程中可能会遇到各式各样的问题,以下是比较常见的几个: 不清楚文件应该保存到哪个目录下。 fail permission denied 文件权限问题。 使用同步函数不清楚怎么获取执行结果。 API提炼 提到文件操作我们会自然而然地想到了API中的FileSystemManager相关的API,我这里用到的函数有以下几个: 下载函数 wx.downloadFile(Object object) 异步函数: FileSystemManager.access(Object object) FileSystemManager.mkdir(Object object) 同步函数: FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 我对同样的业务逻辑分别分别尝试了异步和同步的两种不同的方案,下面我们用一个最简单的下载保存到本地的案例来切入正题。 案例实践 一:获取正确的文件目录路径 当然在保存文件之前我们先要解决一个小问题,那就是我们要保存到哪里?也就是我们自定义的目录。这里我们简单命名其为 [代码]//自定义缓存文件根路径 var rootPath = "......"; [代码] (当然你也可以命名成其他名字) 变量名字可以随便写,不过自定义路径可不能随便写,也不可以在下载的时候直接给一个path路径,否者就会抛出没有权限或找不到文件的异常。所以开发者并不是可以随意的决定自定义文件的路径,这里就不卖关子了,小程序API中有一个很容易被忽略的API, wx.env.USER_DATA_PATH是专门获取文件系统中的用户目录路径的常量值。 这就是我们在小程序中合法的可操作文件的根目录路径: [代码]rootPath = wx.env.USER_DATA_PATH; [代码] 好了到目前为止我们已经知道了我们该往里存储文件了。 定义一下我们下载文件的缓存目录 [代码]var cachePath = rootPath+"/cache"; [代码] 也就是说之后我们下载的文件都会保存到 /cache 目录下,我们在之后的代码中用到的目录路径均为此路径。 二:异步函数实现方案 我们先来用异步函数来实现一下下载保存的流程。 1 下载文件之前我们首先要判断当前目录是否存在,如果目录不存在我们就直接下载文件到该目录下就会抛出 fail permission denied [图片] 这是判断目录存在的代码 [代码] access() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.access({ path: cachePath, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 2 如果目录真实存在那我们当然可以直接使用,如果目录不存在则需要开发者自己创建目录。 [代码] mkdir(){ return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.mkdir({ dirPath: cachePath, recursive: true, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 代码执行完之后我可以验证一下目录是否如我们所愿被创建出来。 开发工具右上角的详情–>基本信息–>文件系统–>当前小程序文件系统根目录 [图片] 点击usr文件夹进入根目录 [图片] 执行完上面代码之后我们可以看一下 cache 文件夹确实已经存在了 [图片] 3 目录也存在了,万事具备只欠下载了 [代码] downloadFile() { let fileUrl = 'https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg'; wx.downloadFile({ url: fileUrl, filePath: cachePath + '/temp.png', success: function(res) { console.log('downloadFile success', res); }, fail: function(err) { console.log('downloadFile fail', err); } }); }, [代码] 那执行完下载的代码之后我们再来看看cache目录 [图片] 这就是我们刚刚下载的图片。 ok,异步的整个下载和文件创建流程就走完了。接下来我们来瞅瞅同步流程中有哪些需要我们注意的。 三: 同步方案 同步方案和异步方案的流程大体一致,都是先判断文件目录是否存在,若不存在则创建目录,存在则执行下载逻辑。 使用同步函数需要特别注意的是怎么去判断函数的执行结果,由于 FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 这两个同步函数没有直接给出任何的返回结果。那我们怎么知道目录是否存在、目录是否被创建成功了呢? 这里我们需要剑走偏锋一下,即利用同步函数抛出的异常来判断结果。 我们直接来看代码 [代码] accessSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.accessSync(cachePath); resolve(); } catch (err) { resolve(err); } }); }, mkdirSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.mkdirSync(cachePath, true); resolve(); } catch (err) { resolve(err); } }); }, [代码] 可以看到我们的代码中多了 try catch 的代码结构,因为同步方法中没有直接返回给我们可用的信息,那我们可以认为同步函数正常执行完的结果为true或success,而进入 catch 后则结果为false或fail亦或根据具体异常具体处理。我们利用同步函数来走一遍下载保存的流程。 [代码] // 同步函数流程 this.accessSync().then(function (err) { if (err) { return that.mkdirSync(); } }).then(function (err) { if (!err) { that.downloadFile() } }); [代码] 可以看到我们利用同步函数下载的图片。 [图片] 总结时刻 我们分别使用异步和同步函数完成了目录的创建和文件的下载等流程。在这个过程中我们特别需要注意几点: 操作文件的根目录是以 wx.env.USER_DATA_PATH 开头的。 使用自定义目录时一定主要不可直接使用,需要增加 判断目录存在、创建目录 两个步骤。 使用同步函数时的执行结果是通过抓取同步函数抛出的异常来进行判断的。在没有给出直接结果的时候要学会利用异常信息来达到目的。 在创建目录时如果该目录存在同样会抛出异常(fail file already exists),这时按照success逻辑继续往下执行即可。 异步方案中介绍了目录查看的步骤和方法,可自行验证。其中usr目录是开发者自定义目录根节点,tmp目录是小程序默认的缓存根节点。 结尾 叙述若有不对或不严谨之处还请不吝指正。
2019-10-31 - ThorUI V1.3.0
小程序二维码 [图片] [图片] ThorUI组件文档参考 http://www.thorui.cn/doc 本项目已同步更新到uni-app,后续会同步支付宝小程序,百度小程序,头条小程序等 更新日志 V1.6.5(2021-05-24) 1.tui-validation(表单验证)优化,新增validator自定义验证配置项,具体查看文档。 2.tui-round-progress(圆形进度条)组件优化,修复已知问题。 3.tui-cascade-selection(级联选择器)组件优化,修复已知问题。 4.tui-tabs(标签页)组件优化,选项卡可设置数字角标。 ===================== 【ThorUI示例V1.1.0】更新: 1.tui-org-tree(组织架构树)组件优化,可控制节点内容排版方式、节点选中状态、展开收起子节点,具体查看文档。 2.新增tui-form(表单)组件,主要用于表单验证。 3.新增tui-input(输入框)组件,原生input组件增强。 4.新增tui-textarea(多行输入框)组件。 5.新增tui-label(标签)组件,用来改进表单组件的可用性。 6.新增tui-radio(单项选择器)组件。 7.新增tui-checkbox(多项选择器)组件。 8.新增tui-switch(开关)组件。 9.新增tui-picker(选择器)组件,支持1~3级数据。 10.新增tui-landscape(压屏窗)组件。 11.新增tui-segmented-control(分段器)组件。 12.新增tui-notice-bar(通告栏)组件。 13.新增tui-alerts(警告框)组件。 14.新增tui-request(数据请求)封装,支持Promise,支持请求拦截和响应拦截,支持请求未结束之前阻止重复请求等。 15.tui-utils(工具类)优化,具体查看文档。 16.新增tui-row组件,配合组件tui-col组件使用(24栅格化布局)。 17.新增tui-tree-view(树型菜单)组件。 18.新增tui-charts-column(柱状图-css版)组件。 19.新增tui-charts-bar(横向柱状图-css版)组件。 20.新增tui-charts-line(折线图表-css版)组件。 21.新增tui-charts-pie(饼状图表-css版)组件。 22.tui-lazyload-img(图片懒加载)组件优化,修复已知问题。 23.新增tui-pagination(分页器)组件。 部分功能截图 [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] V1.3.0 1.新增倒计时组件:时分秒倒计时,支持设置大小,颜色等。 2.新增分隔符组件:Divider分隔符,可设置占据高度,线条宽度,颜色等。 3.新增卡片轮播:包含顶部轮播,秒杀商品轮播等。 4.已知问题修复以及优化。 V1.2.1 1.新增组件Modal弹框:可设置按钮数,按钮样式,提示文字样式等,还可自定义弹框内容。 2.修复部分已知bug。
2021-06-01 - Vue3 全家桶 + TS+ Vite2 + element-plus 搭建简洁时尚的博客网站实战及踩坑记
[图片] 五一期间,花了 3 天时间,边学 Vue3 和 Vite2,边重构自己的项目,终于都用 Vue3 + TypeScript + Vite2 + Vuex4 + Vue-Router4 + element-plus 重构完啦! 项目地址: http://github.crmeb.net/u/yi [图片] 效果效果图: pc 端 [图片] 移动端 [图片] 完整效果请看: biaochenxuying.cn 功能 已经完成功能 登录 注册 文章列表 文章归档 标签 关于 点赞与评论 留言 历程 文章详情(支持代码语法高亮) 文章详情目录 移动端适配 github 授权登录 前端主要技术 所有技术都是当前最新的。 vue:^3.0.5typescript : ^4.1.3element-plus: ^1.0.2-beta.41vue-router : ^4.0.6vite: ^2.2.3vuex: ^4.0.0axios: ^0.21.1highlight.js: ^10.7.2marked:^2.0.3 1. 初化化项目 用 vite-app 创建项目 yarn create vite-app # 或者 npm init vite-app 然后按照提示操作即可! 进入项目,安装依赖 cd yarn # 或 npm i 运行项目 yarn dev 打开浏览器 http://localhost:3000 查看 2. 引入 TypeScript 在创建项目的时候可以 TypeScript 的,如果你选择了 TypeScript ,可以忽略第 2 个步骤。 加入 ts 依赖 yarn add --dev typescript 在 项目根目录下创建 TypeScript 的配置文件 tsconfig.json { "compilerOptions": { // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。 "allowSyntheticDefaultImports": true, // 解析非相对模块名的基准目录 "baseUrl": ".", "esModuleInterop": true, // 从 tslib 导入辅助工具函数(比如 __extends, __rest等) "importHelpers": true, // 指定生成哪个模块系统代码 "module": "esnext", // 决定如何处理模块。 "moduleResolution": "node", // 启用所有严格类型检查选项。 // 启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict, // --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。 "strict": true, // 生成相应的 .map文件。 "sourceMap": true, // 忽略所有的声明文件( *.d.ts)的类型检查。 "skipLibCheck": true, // 指定ECMAScript目标版本 "target": "esnext", // 要包含的类型声明文件名列表 "types": [ ], "isolatedModules": true, // 模块名到基于 baseUrl的路径映射的列表。 "paths": { "@/*": [ "src/*" ] }, // 编译过程中需要引入的库文件的列表。 "lib": [ "ESNext", "DOM", "DOM.Iterable", "ScriptHost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules" ] } 在 src 目录下新加 shim.d.ts 文件 /* eslint-disable */ import type { DefineComponent } from 'vue' declare module '*.vue' { const component: DefineComponent<{}, {}, any> export default component } 把 main.js 修改成 main.ts 在根目录,打开 Index.html <script type="module" src="/src/main.js"></script> 修改为: <script type="module" src="/src/main.ts"></script> 3. 引入 eslint 安装 eslint prettier 依赖 [代码]@typescript-eslint/parser @typescr ipt-eslint/eslint-plugin[代码] 为 eslint 对 typescript 支持。 yarn add --dev eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue @typescript-eslint/parser @typescr ipt-eslint/eslint-plugin 在根目录下建立 eslint 配置文件: .eslintrc.js module.exports = { parser: 'vue-eslint-parser', parserOptions: { parser: '@typescript-eslint/parser', ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } }, extends: [ 'plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended' ], rules: { '@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-empty-function': 'off', 'vue/custom-event-name-casing': 'off', 'no-use-before-define': 'off', // 'no-use-before-define': [ // 'error', // { // functions: false, // classes: true, // }, // ], '@typescript-eslint/no-use-before-define': 'off', // '@typescript-eslint/no-use-before-define': [ // 'error', // { // functions: false, // classes: true, // }, // ], '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^h$', varsIgnorePattern: '^h$' } ], 'no-unused-vars': [ 'error', { argsIgnorePattern: '^h$', varsIgnorePattern: '^h$' } ], 'space-before-function-paren': 'off', quotes: ['error', 'single'], 'comma-dangle': ['error', 'never'] } }; 建立 prettier.config.js module.exports = { printWidth: 100, tabWidth: 2, useTabs: false, semi: false, // 未尾逗号 vueIndentScriptAndStyle: true, singleQuote: true, // 单引号 quoteProps: 'as-needed', bracketSpacing: true, trailingComma: 'none', // 未尾分号 jsxBracketSameLine: false, jsxSingleQuote: false, arrowParens: 'always', insertPragma: false, requirePragma: false, proseWrap: 'never', htmlWhitespaceSensitivity: 'strict', endOfLine: 'lf' } 4. vue-router、vuexnpm install vue-router@4 vuex 4.1 vuex在根目录下创建 store/index.ts import { InjectionKey } from 'vue' import { createStore, Store } from 'vuex' export interface State { count: number } export const key: InjectionKey> = Symbol() export const store = createStore({ state() { return { count: 0 } }, mutations: { increment(state) { state.count++ } } }) main.ts 修改 import { createApp } from 'vue' import { store, key } from './store' import App from './App' import './index.css' const app = createApp(App) app.use(store, key) app.mount('#app') components/HelloWord.vue 修改 import { defineComponent, computed } from 'vue' import { useStore } from 'vuex' import { key } from '../store' export default defineComponent({ name: 'HelloWorld', props: { msg: { type: String, default: '' } }, setup() { const store = useStore(key) const count = computed(() => store.state.count) return { count, inCrement: () => store.commit('increment') } } }) 4.2 vue-router 在 src 目录下建立 router/index.ts,内容如下: import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import HelloWorld from "../components/HelloWorld.vue"; const routes: Array = [ { path: "/", name: "HelloWorld", component: HelloWorld, }, { path: "/about", name: "About", // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "About" */ "../components/About.vue") } ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); export default router; 再新建一个 components/About.vue 文件,内容如下: import { defineComponent } from 'vue' export default defineComponent({ name: 'About', data() { return { msg: 'Hello Vue 3.0 + Vite!' } }, setup() {} }) 再修改 main.ts import { createApp } from 'vue' import { store, key } from './store' import router from "./router"; import App from './App' import './index.css' const app = createApp(App) app.use(store, key) app.use(router) app.mount('#app') 再访问 http://localhost:3000/ [图片] 和 http://localhost:3000/about 即可 [图片] 5. 加入 Element Plus 5.1 安装 element-plus 全局安装 npm install element-plus --save 5.2 引入 Element Plus你可以引入整个 Element Plus,或是根据需要仅引入部分组件。我们先介绍如何引入完整的 Element。 完整引入 在 main.js 中写入以下内容: import { createApp } from 'vue' import ElementPlus from 'element-plus'; import router from "./router"; import 'element-plus/lib/theme-chalk/index.css'; import App from './App.vue'; import './index.css' const app = createApp(App) app.use(ElementPlus) app.use(router) app.mount('#app') 以上代码便完成了 Element Plus 的引入。需要注意的是,样式文件需要单独引入。 按需引入 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。 首先,安装 babel-plugin-component: npm install babel-plugin-component -D 然后,将 .babelrc 修改为: { "plugins": [ [ "component", { "libraryName": "element-plus", "styleLibraryName": "theme-chalk" } ] ] } 接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容: import { createApp } from 'vue' import { store, key } from './store'; import router from "./router"; import { ElButton, ElSelect } from 'element-plus'; import App from './App.vue'; import './index.css' const app = createApp(App) app.component(ElButton.name, ElButton); app.component(ElSelect.name, ElSelect); /* or * app.use(ElButton) * app.use(ElSelect) */ app.use(store, key) app.use(router) app.mount('#app') app.mount('#app') 更详细的安装方法请看 快速上手。 5.3 全局配置 在引入 Element Plus 时,可以传入一个全局配置对象。 该对象目前支持 [代码]size[代码] 与 [代码]zIndex[代码] 字段。[代码]size[代码] 用于改变组件的默认尺寸,[代码]zIndex[代码] 设置弹框的初始 z-index(默认值:2000)。按照引入 Element Plus 的方式,具体操作如下: 完整引入 Element: import { createApp } from 'vue' import ElementPlus from 'element-plus'; import App from './App.vue'; const app = createApp(App) app.use(ElementPlus, { size: 'small', zIndex: 3000 }); 按需引入 Element: import { createApp } from 'vue' import { ElButton } from 'element-plus'; import App from './App.vue'; const app = createApp(App) app.config.globalProperties.$ELEMENT = option app.use(ElButton); 按照以上设置,项目中所有拥有 [代码]size[代码] 属性的组件的默认尺寸均为 'small',弹框的初始 z-index 为 3000。 5.4 配置 vite.config.ts 其中 proxy 和 alias 是和 vue-cli 区别比较大的地方。 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import styleImport from 'vite-plugin-style-import' import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), styleImport({ libs: [ { libraryName: 'element-plus', esModule: true, ensureStyleFile: true, resolveStyle: (name) => { return `element-plus/lib/theme-chalk/${name}.css`; }, resolveComponent: (name) => { return `element-plus/lib/${name}`; }, } ] }) ], /** * 在生产中服务时的基本公共路径。 * @default '/' */ base: './', /** * 与“根”相关的目录,构建输出将放在其中。如果目录存在,它将在构建之前被删除。 * @default 'dist' */ // outDir: 'dist', server: { // hostname: '0.0.0.0', host: "localhost", port: 3001, // // 是否自动在浏览器打开 // open: true, // // 是否开启 https // https: false, // // 服务端渲染 // ssr: false, proxy: { '/api': { target: 'http://localhost:3333/', changeOrigin: true, ws: true, rewrite: (pathStr) => pathStr.replace('/api', '') }, }, }, resolve: { // 导入文件夹别名 alias: { '@': path.resolve(__dirname, './src'), views: path.resolve(__dirname, './src/views'), components: path.resolve(__dirname, './src/components'), utils: path.resolve(__dirname, './src/utils'), less: path.resolve(__dirname, "./src/less"), assets: path.resolve(__dirname, "./src/assets"), com: path.resolve(__dirname, "./src/components"), store: path.resolve(__dirname, "./src/store"), mixins: path.resolve(__dirname, "./src/mixins") }, } }) 踩到坑 在 [代码]npm run dev[代码] 打包时不报错,但是在 [代码]npm run build[代码] 时却报错了,build 的时候会把 [代码]node_modules[代码] 里面的文件也编译,所以挺多 element-plus 的类型文件报错了。 把 [代码]tsconfig.json[代码] 里面的 [代码]include[代码] 和 [代码]exclude[代码] 修改一下就不会了,配置如下 { "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, // 忽略 this 的类型检查, Raise error on this expressions with an implied any type. "noImplicitThis": false, "resolveJsonModule": true, "esModuleInterop": true, "lib": ["esnext", "dom"], "types": ["vite/client"] }, "include": ["/src/**/*.ts", "/src/**/*.d.ts", "/src/**/*.tsx", "/src/**/*.vue"], // ts 排除的文件 "exclude": ["node_modules"] } Vue3 + vite2 打包出来的文件和原来 vue2 版的差别也挺大的,由原来 2.5M 直接变成了 1.8M ,amazing! [图片] 最后 [图片] 项目代码大多都是 2 年前的,还有很多可以优化的地方,这次重构的过程没对原来的样式和代码做什么改动,没那么多时间,加上我懒 😂 这次就升级了主要框架与相应的 ui 库,过了一遍 Vue3 中的 API,发现很多 Vue3 中新的 API 都用不上,主要是要熟练一下 Vue3 和 Vite2 项目搭建,这假期也算有所收获。 具体项目源码请看: http://github.crmeb.net/u/yi 至此,一个基于 Vue3 全家桶 + Vite2 + TypeScript + Element Plus 的开发环境已经搭建完毕,现在就可以编写代码了,各个组件的使用方法请参阅它们各自的文档。 不得不说 Vue3 + Element Plus + Vite + TypeScript 是真的香! 推荐一个 Vue3 相关的资料汇总: Vue3 的学习教程汇总、源码解释项目、支持的 UI 组件库、优质实战项目,相信你会挖到矿哦! 参考文章:vue3 + vite + typescript + eslint + jest 项目配置实践 推荐阅读 TypeScript 中提升幸福感的 10 个高级技巧 作者:天明夜尽 更多精彩技术文章汇总在我的http://github.crmeb.net/u/yi ,持续更新,欢迎点个 star 订阅收藏。
2021-05-08 - 小程序富文本解析
wxParse 微信小程序富文本解析 原因 由于原作者仓库 wxParse 不再维护,我们项目中商品信息展示又是以wxParse这个用做富文本解析的; 于是乎,决定采用 递归Component 的方式对其进行重构一番; 原项目使用的 [代码]template[代码] 模板的方式渲染节点,存在以下问题: 节点渲染支持到12层,超出会原样输出 [代码]html[代码] 代码; 每一层级的循环模板都重复了一遍所有的可解析标签,代码十分臃肿。 [代码]li[代码]标签不支持 [代码]ol[代码] 有序列表渲染(统一采用的是 [代码]ul[代码] 无序列表),[代码]a[代码]标签无法实现跳转,也无法获取点击事件回调等等; 节点渲染没有绑定 [代码]key[代码] 值,一是在开发工具看到一堆的 [代码]warning[代码]信息(看着十分难受),二是节点频繁删除添加,无法比较[代码]key值[代码],造成 [代码]dom[代码] 节点频繁操作。 功能标签 目前该项目已经可以支持以下标签的渲染: audio标签(可自行更换组件样式,暂时采用微信公众号文章的[代码]audio[代码]音乐播放器的样式处理) ul标签 ol标签 li标签 a标签 img标签 video标签 br标签 button标签 h1, h2, h3, h4标签 文本节点 其余块级标签 其余行级标签 支持 npm包 引入 [代码]npm install --save wx-minicomponent [代码] 使用 原生组件使用方法 克隆 项目 代码,把 components目录 拷贝到你的小程序根目录下面; 在你的 page页面 对应的 [代码]json[代码] 文件引入 [代码]wxParse[代码] 组件 [代码]{ "usingComponents": { "wxParse": "/components/wxParse/wxParse" } } [代码] 组件调用 [代码]<wxParse nodes="{{ htmlText }}" /> [代码] npm组件使用方法 安装组件 [代码]npm install --save wx-minicomponent [代码] 小程序开发工具的 [代码]工具[代码] 栏找到 [代码]构建npm[代码],点击构建; 在页面的 json 配置文件中添加 [代码]wxParse[代码] 自定义组件的配置 [代码]{ "usingComponents": { "wxParse": "/miniprogram_npm/wx-minicomponent/wxParse" } } [代码] [代码]wxml[代码] 文件中引用 wxParse [代码]<wxParse nodes="{{ htmlText }}" /> [代码] 提示:详细步骤可以参考小程序的npm使用文档 补充组件:代码高亮展示组件使用 在 [代码]page[代码]的 [代码]json[代码] 文件里面引入 [代码]highLight[代码] 组件 原生引入: [代码]{ "usingComponents": { "highLight": "/components/highLight/highLight" } } [代码] npm组件引入: [代码]{ "usingComponents": { "highLight": "/miniprogram_npm/wx-minicomponent/highLight" } } [代码] 组件调用 [代码]<highLight codeText="{{codeText}}" /> [代码] 参数文档 wxParse:富文本解析组件 参数 说明 类型 例子 nodes 富文本字符 String “<div>test</div>” language 语言 String 可选:“html” | “markdown” (“md”) 受信任的节点 节点 例子 audio <audio title=“我是标题” desc=“我是小标题” src=“https://media.lycheer.net/lecture/25840237/5026279_1509614610000.mp3?0.1” /> a <a href=“www.baidu.com”>跳转到百度</a> p div span li ul ol img button h1 h2 h3 h4 … highLight:代码高亮解析组件 参数 说明 类型 例子 codeText 原始高亮代码字符 String “var num = 10;” language 代码语言类型 String 可选值:“javascript/typescript/css/xml/sql/markdown” 提示:如果是html语言,language的值为xml wxAudio:仿微信公众号文章音频播放组件 参数 说明 类型 例子 title 标题 String “test” desc 副标题 String “sub test” src 音频地址 String 示例展示 富文本解析 html文本解析实例 [图片] markdown文本解析实例 [图片] 代码高亮 [图片] 更新历史 2020-5-31 迁移utils目录到wxParse目录下; 富文本增加markdown文本解析支持; 2020-5-21: 富文本组件image标签添加loading过渡态,优化图片加载体验 2020-5-17: 完善组件参数文档,增加wxParse对audio标签标题,副标题的解析功能 2020-5-13: 增加css,html,ts,sql,markdown代码高亮提示支持 2020-5-6: 增加图片预览功能 项目地址 项目地址:https://github.com/csonchen/wxParse
2020-06-01 - 微信小程序原生开发辅助框架 LWX
项目介绍 作者开发了一年多的小程序,在开发过程中遇到了很多的坑与不方便之处,同时又对原生开发有着一定的执著,但是对于习惯了我这种用惯了vue的人来说,原生小程序中的一些写法确实让人感到难受,我想大家在进行原生开发时也会面临着一些不方便之处,例如wxss写法不支持嵌套,不支持变量、混合、函数,不支持双向绑定等等一些问题。于是本着不放弃原生开发并解决一些开发中的问题,一个原生微信小程序开发辅助框架 LWX 应运而生,希望能帮助大家解决一些开发中的痛点与不方便之处同时也借此分享一些我的小程序开发心得,同时希望大家能多多交流一起完善这个项目,让原生小程序开发更加丝滑。 项目地址 https://github.com/lvyueyang/lwx 项目特点 只需要几分钟即可快速上手,LWX 就像一个游戏辅助一样来帮助你快速通关微信小程序开发 可以直接分离源码,快速融入进现有项目 整个项目不到100k的代码,不会影响项目大小 配合文档,源码易懂,可快速定制修改 项目启动 全局安装gulp [代码]cnpm i -g gulp [代码] 安装依赖 [代码]cnpm i [代码] 启动实时编译sass [代码]gulp serve [代码] 立即编译全部sass [代码]gulp all [代码] 创建新页面 [代码]gulp create -name=<页面名称> [代码] 使用微信开发者工具打开app文件夹,即可开始开发 sass与wxss相关 sass编译 默认只编译在[代码]app/style/[代码]下的子文件, [代码]app/style/modules[代码] 下的文件不会编译。 在 [代码]app/pages/[代码]下的二级scss文件会被编译, 编译后的文件存在于同级目录,如果新增编译文件,需要在gulpfile.js 文件中 [代码]watch_files[代码] 数组内添加对应目录。 为了减少编译后的体积,可以直接在scss文件中直接引入wxss文件(必须添加[代码]*.wxss[代码]后缀),编译时会自动忽略对wxss的编译,在wxss中直接显示引用。 例如: [代码]@import "../../style/modules/color"; @import "../../style/animation.wxss"; .list { padding: 10PX; .item { padding: 20px; } } [代码] 编译为: [代码]@import "../../style/animation.wxss"; .list { padding: 10PX; } .list .item { padding: 20rpx; } [代码] 全局样式 在[代码]app/style[代码]文件夹内存在着 [代码]rest.scss[代码] [代码]icon.scss[代码] [代码]button.scss[代码] 这三个文件默认是全局引入的,如不需要可以在 [代码]app/app.wxss[代码] 中删除引用。 接下来对这三个样式进行简单描述 rest.scss 对全部标签设置了 [代码]box-sizing: border-box;[代码] (至于为什么不使用 * 大家亲自尝试就知道了) flex布局简写样式,tab样式,link-list样式,form-wrap 请查看源码 icon.scss 此处是为了举例如何引入字体图标,因小程序不支持本地路径,所有将字体文件转成base64后引用即可。 以iconfont为例 在浏览器中打开生成的连接,@font-face 下只保留base64字符串引入即可 button.scss 原生的button按钮实在是太丑了,此处使用 .btn class对button按钮进行美化,使用方式和bootstrap类似 此处只做概述,细节请查看源码 全局注入 场景一:双向绑定 在 app/hook 中存在 mixins 和 beforeHook 文件夹,此处是为了实现上文中的双向绑定和一些其他的全局可调用的函数方法例如: 平常在为一个输入框添加类似双向绑定的功能时需要这样写 [代码]<input type="text" value="{{name}}" bindinput="handlerInput" /> [代码] [代码]Page({ data: { name: '' }, onLoad() { }, handlerInput(event) { const {value} = event.detail this.setData({ name: value }) } }) [代码] 如果整个项目只有一处或者很少那么这样写没什么问题,但是如果很多页面需要那么你就需要写很多 [代码]handlerInput[代码] 方法这样就很费事了。但是在 LWX 中你不需要在js中写 [代码]handlerInput[代码]方法了,[代码]LWX[代码] 使用函数拦截的方式为每个Page 和 Component 注入了一个 [代码]$MixinVModel[代码] 方法。此时你只需要在 wxml 中这样写,不需要在js写了。 [代码]<input type="text" value="{{name}}" data-model="name" bindinput="$MixinVModel" /> [代码] 只需要在 input中添加 一个[代码]data-model[代码]属性,对应data中的值,并且更换 ``handlerInput[代码]为[代码]$MixinVModel`即可。 同时也支持绑定对象,举例如下 [代码]<input type="text" value="{{form.name}}" data-model="form.name" bindinput="$MixinVModel" /> [代码] [代码]$MixinVModel[代码]同时还支持以下表单组件 checkbox-group radio-group picker picker-view slider switch textarea input LWX 同时内置了其他类似$MixinVModel的方法 大家可以在 /app/hook/mixins/global.js 中查看,如果需要扩展也可以在在此处自行添加修改 mixins API [代码]$MixinVModel[代码] 输入框双向绑定 标签属性名称:[代码]data-model[代码] data中对应的值(类似v-model) 必填 [代码]$MixinDownHttpImage[代码] 下载网络图片 标签属性名称:[代码]data-url[代码] 图片地址 必填 [代码]$MixinCopyText[代码] 复制文字 标签属性名称:[代码]data-text[代码] 要复制的文字 必填 [代码]$MixinToTell[代码] 拨打电话 标签属性名称:[代码]data-tell[代码] 电话号码 必填 [代码]$MixinScrollToSelector[代码] 跳转至选择器位置 标签属性名称:[代码]data-selector[代码] 选择器 必填 [代码]$MixinBackTop[代码] 返回顶部 场景二:每个页面在初始化时都执行一个相同的方法 这个场景是我在开发公司项目时遇到的问题,领导想要通过后端为每个页面实时的配置分享语和分享图,后端提供了一个接口,通过页面路由来获取对应的分享语和分享图,演示函数如下: [代码]import api from '../../api/index' let shareData = {} Page({ data: {}, onLoad() { this.setShareMessage() }, async setShareMessage() { try { const data = await api.get(this.route) shareData = { title: data.title, path: data.path, imageUrl: data.imageUrl, } } catch (e) { console.error(e) } }, onShareAppMessage() { return shareData } }) [代码] 如上所示,这是为某一个页面设置信息,如果所有的都要设置改怎么办呢?一个个的加?太费事儿了。 在 LWX 中你可以这样实现(类似[代码]vue-router[代码]中的路由导航守卫): 在 [代码]app/hook/beforeHook[代码] 中新建 [代码]setShare.js[代码] [代码]import api from '../../api/index' // 设置服务端配置的分享语 // this为每个Page中的this export default async function () { try { const data = await api.get(this.route) this.onShareAppMessage = function () { return { title: data.title, path: data.path, imageUrl: data.imageUrl, } } } catch (e) { } } [代码] 之后在[代码]app/hook/index.js[代码] 中引用 [代码]setShare.js[代码] [代码]... import setShare from './beforeHook/setShare' ... const hookOption = { ... onLoad(option) { setShare.call(this) }, ... } [代码] 如上方式 即可在 **LWX ** 中解决了此种需求,大家也可以发挥想象随意扩展。 此处示例在源码中已经存在,可以直接查看源码 与web-view内页面交互(持续完善) 因小程序存在大小限制和发版审核机制,有时候我们需要嵌入H5页面来应对或者复用一些场景。 LWX为小程序与H5互相调用交互提供了一套解决方案: 快速跳转至H5页面 在 [代码]app/util/toWeb[代码] 中存在 [代码]toWeb[代码] 方法,只需要传入对应参数即可直接跳转至对应H5页面。 在H5中如何设置页面分享语 在引入wx-sdk并初始化完成的前提下 h5中执行如下方法即可 [代码]function postShareData() { const shareData = { title: '这是分享页面', path: '', pathType: 'miniPath', // 设置为 miniPath 时,path为小程序路径地址。将会分享小程序内的路径地址,不填path可设置为H5地址 imageUrl: '' } wx.miniProgram.postMessage({data: {shareData}}) } [代码] 授权组件 为减少在wxml中重复书bind方法LWX 提供 [代码]l-auth[代码]授权组件使用步骤如下 此组件已经全局注册,直接引用即可 引入组件 [代码]<l-auth id="Auth"></l-auth> [代码] 使用js调用 (获取手机号为例) [代码]const auth = this.selectComponent('#Auth') auth.show({ openType: 'getPhoneNumber', content: '', subtitle: '', mask: '', confirmText: '', cancelText: '', showCancel: '', success: res => { console.log(res) }, fail: e => { console.error(e) } }) [代码] 参数解析 code 名称 必填 默认 类型 备注 openType 唤起的授权类型 String 对应 [代码]button[代码]的[代码]openType[代码] 仅支持[[代码]getPhoneNumber[代码],[代码]getUserInfo[代码]] content 弹出模态框的内容 否 String subtitle 弹出模态框的子内容 否 String mask 点击遮罩层允许关闭 否 false Boolean confirmText 确认按钮文字 否 确定 String cancelText 取消按钮文字 否 取消 String showCancel 是否显示取消按钮 否 false Boolean success 授权成功的回调 Function fail 授权失败的回调 Function 自定义header组件 LWX内提供 [代码]navigationStyle = "custom"[代码] 时自定义header组件(已经为大家适配了iPhoneX系列的齐刘海) 此组件已经全局注册,直接引用即可 [代码]<l-header></l-header> [代码] 此组件只提供适配实现,其他自定义属性请按照自己业务自行定制 modal组件 LWX内提供 自定义modal组件,用于 [代码]l-auth[代码],也可用于直接引用 此组件已经全局注册,直接引用即可 [代码]<l-modal></l-modal> [代码] 属性 code 名称 必填 类型 默认 show 显示隐藏 否 Boolean false headerHide 是否隐藏头部 否 Boolean false title 标题 否 String backgroundOpacity 遮罩层透明度 否 String 0.4 mask 点击遮罩层是否关闭 否 Boolean false closeIcon 关闭按钮是否显示 否 Boolean true 其他 LWX在每个Page的Component的data中都注入了 [代码]$ipx[代码] 用于判断是否为ipx系列齐刘海手机,方便大家适配ipx系列齐刘海手机 内置 [代码]Day.js[代码] 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样, 文档地址 util文件夹中内置 canvas 相关的4个方法,在只做海报时应该会有用到 最后 感谢大家的阅读和使用,大家对 LWX有什么改进意见或者想法可以联系我(QQ:975794403)进行交流,一起完善 LWX
2020-04-03 - 微信开发工具直接编译scss文件
前言 以前微信开发工具不支持打卡scss文件,每次一个小改动,都要切换其他编辑器编译,感觉比较麻烦.最近 RC Build (1.02.2001191) 的开发工具支持打开scss文件,这算是一个福音,然而如何编译成wxss却没有说明,所以自己利用gulp在开发工具中直接编译,很方便. 使用 1、在项目与app.js同级目录中新建文件gulpfile.js 加入内容如下: var gulp = require('gulp'); var sass = require('gulp-sass'); var rename = require('gulp-rename') var changed = require('gulp-changed') var watcher = require('gulp-watch') //自动监听 gulp.task('default', gulp.series(function() { watcher('./pages/**/*.scss', function(){ miniSass(); }); })); //手动编译 gulp.task('sass', function(){ miniSass(); }); function miniSass(){ return gulp.src('./pages/**/*.scss')//需要编译的文件 .pipe(sass({ outputStyle: 'expanded'//展开输出方式 expanded })) .pipe(rename((path)=> { path.extname = '.wxss' })) .pipe(changed('./pages'))//只编译改动的文件 .pipe(gulp.dest('./pages'))//编译 .pipe(rename((path)=> { console.log('编译完成文件:' + 'pages\\' + path.dirname + '\\' + path.basename + '.scss') })) } 复制代码 2、打开命令行,进入gulpfile.js所在目录,执行如下命令 [代码]npm install[代码][代码]gulp-sass[代码][代码]gulp-rename[代码][代码]gulp-changed[代码]3、执行监听命令 [代码]gulp[代码] 会监听pages目录所有的scss文件变动,保存后会自动编译.
2020-02-24 - 微信开发者工具创建的Typescript小程序需要改造目录结构后才能使用第三方组件
微信开发者工具创建的typescript小程序目录结构并不能良好的使用npm的“自定义组件”,想要漂亮地使用“自定义组件”,得自行改造工程结构 注:这里是指wxml中使用的“自定义组件”,不是ts或js中可调用的纯js 问题现象分析如下: 微信开发者工具,创建出的Typescript小程序工程结构如下: [代码]|--miniprogram | |--pages | |--app.js | |--app.wxss | |--app.json |--typings | |--wx | |--index.d.ts | |--lib.wa.es6.d.ts |--package.json |--project.config.js |--ts.json [代码] 这样的结构,小程序根目录在miniprogram,导致package.json不在小程序根目录内,也即node_modules目录中的第三方组件不在小程序根目录,违背了npm支持中node_modules位置的要求:npm 支持-使用npm包 此时,如果app.json或index.json中,无论如何引用不到node_modules中的第三方组件(注意是第三方开发的微信“自定义组件”,如vant-weapp,而纯js是可用的) 如: [图片] 图中的"…/node_modules"无论如何修改,控制台也会报类似的错误: [图片] 问题原因: 经过测试,存在两个约束导致了以上问题: app.json或index.json(以及任何页面的配置文件)的"usingComponents"不能引用node_modules目录下的组件; "usingComponents"只能引用小程序根目录(此例是miniprogram)及子目录下的组件。 通过以下步骤,就可以验证: 将node_modules下的vant-weapp目录复制到miniprogram目录下,"usingComponents"修改为其相对位置,组件即可用: [代码]|--miniprogram | |--pages | |--app.js | |--app.wxss | |--app.json | |--want-weapp |--typings | |--wx | |--index.d.ts | |--lib.wa.es6.d.ts |--package.json |--project.config.js |--ts.json [代码] [图片] 成功使用自定义组件的效果: [图片] 2. 如果vant-weapp放在miniprogram以外的目录,仍然引用不到。 但是,有代码洁癖的同学肯定不能满足于,把所有的依赖都手工复制到miniprogram目录这么弱智的操作方法吧? 解决方案: 怎样才能做到npm安装的组件,可以直接在小程序中引用呢? 其实答案就在npm 支持这篇文章里,只不过其写的比较含糊,没有解释清楚细节,总结起来就是对模板工程做两步改造,分别解决上面提到的两个问题: 将小程序根目录即miniprogram移动到与package.json同级别; 使用“npm构建”功能(将node_modules目录的组件构建到miniprogram_npm目录下)构建后,再直接引用组件即可使用。 操作步骤如下: 第一步:移动miniprogram目录下的所有文件到根目录(与package.json同级): [代码]|--pages |--app.js |--app.wxss |--app.json |--want-weapp |--typings | |--wx | |--index.d.ts | |--lib.wa.es6.d.ts |--package.json |--project.config.js |--ts.json [代码] 第二步:修改project.config.json,去掉下面这行: [代码]"miniprogramRoot": "miniprogram/", [代码] 或者修改为 [代码]"miniprogramRoot": "./", [代码] 第三步:修改tsconfig.json中的include/exclude以确保自己的ts代码被编译: [代码] "include": [ "./miniprogram/**/*.ts" ], "exclude": [ "node_modules", "miniprogram_dist", "miniprogram_npm", "**/*.spec.ts", "typings/**/*.d.ts" ] [代码] 改为 [代码]"include": [ "./**/*.ts" ], "exclude": [ "node_modules", "miniprogram_dist", "miniprogram_npm", "**/*.spec.ts", "typings/**/*.d.ts" ] [代码] 说明:include目录外移一层,同时exclude掉typings和miniprogram_npm目录 第四步:使用微信开发者工具的“工具-构建npm”功能构建组件到miniprogram_npm目录 第五步:在app.json(或者页面的json配置)里面引入组件 [代码] "usingComponents": { "van-button": "vant-weapp/button/index" } [代码] 注意:这里使用相对于miniprogramnpm的目录就可以了,并不需要相对于app.json文件 后记: 微信开发者工具创建出的typescript小程序,本意应该是将小程序根目录与typescript、node环境隔离开来,提供一个清晰的目录结构,但是对npm集成、“自定义组件”支持的不佳,说明了微信团队在“自定义组件”,npm支持,typescript支持还处在起步阶段,团队间的磨合还不到位,文档也不够透彻,当然这需要时间去改进。 我希望微信小程序团队通过以下几点,能很快地原生支持typescript+npm+第三方自定义组件: 1、调整微信开发者工具的typescript小程序模板工程结构,或者反过来让支持现在的工程结构下使用第三方的npm自定义组件; 2、完善npm支持相关的文档,如果自己完善不来,其实可以用wiki的方式,鼓励广大开发者一起来完善
2019-08-11 - 2019-03-21