爱玩的跑步鞋 · 为iframe正名,你可能并不需要微前端 ...· 2 周前 · |
文武双全的爆米花 · NodeJS 将 Base64 或 ...· 1 周前 · |
另类的爆米花 · fs如何将buffer转成file文件对象 ...· 1 周前 · |
稳重的警车 · 【QT】如何自定义QMessageBox的窗 ...· 1 周前 · |
苦闷的围巾 · const (C++) | ...· 22 小时前 · |
睡不着的针织衫 · 2024年女篮奥运资格赛:获得2024年巴黎 ...· 昨天 · |
狂野的电池 · 签证免办情况(2016年3月21日更新)· 3 周前 · |
谦和的凉面 · 【云度】云度汽车_云度报价及图片_云度汽车报 ...· 1 年前 · |
爱看书的火锅 · 131期阿旺福彩3D预测奖号:跨度胆码和六码 ...· 1 年前 · |
没人理的红茶 · 全员养猫的游戏公司,会做出怎样的游戏? - ...· 1 年前 · |
引言unplugin-vue-components 是一款能帮助组件自动导入的库,简单点的说,你不需要使用import xx from 'xxx.vue' 这行语句也能实现导入的效果。<script setup lang="ts"> import ScreenAdpter from '@compontents/ScreenAdpter/index.vue' import Play from '@components/Play/index.vue' </script> <template> <ScreenAdpter> <Play></Play> </ScreenAdpter> </template> <style scoped></style>等同于以下效果<script setup lang="ts"> </script> <template> <ScreenAdpter> <Play></Play> </ScreenAdpter> </template> <style scoped></style>效果这里需要实现的效果如下:发现问题但是问题来了,使用pnpm的用户,我相信许多人是实现不了这上效果的。当所有的配置文件配好,然后就出现下面的效果啦!!!问题效果你会发现,在组件使用的地方的类型是any, 当你去unplugin-vue-components 这里面点击组件是可以进去的,那么怎么来解决这个引用问题呢?解决问题刨根问底既然组件显示的类型是any,那么咱们先看下生产的类型声明文件。// generated by unplugin-vue-components // We suggest you to commit this file into source control // Read more: types(defineComponent): support for expose component types by pikax · Pull Request #3399 · vuejs/core import '@vue/runtime-core' export {} declare module '@vue/runtime-core' { export interface GlobalComponents { Play: typeof import('./components/Play/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScreenAdpter: typeof import('./components/ScreenAdpter/index.vue')['default'] 在自动生成的 components.d.ts 文件中的 declare module '@vue/runtime-core' 声明,在 pnpm 中只能访问项目的顶级依赖,而 @vue/runtime-core 是 vue 模块下的依赖,不是顶级依赖,导致声明语句失效。(yarn 和 npm 的 node_modules 平铺目录结构允许访问所有依赖)解决方案 (首选)在目录的根目录中创建或编辑.npmrc文件,并在其中添加以下行:public hoist pattern[]=@vue/runtime core(不推荐)在目录的根目录中创建或编辑.npmrc文件,并在其中添加以下行:shamefully-hoist=true(这样做将使所有嵌套依赖项都可用作顶级依赖项)(不推荐)运行pnpm add@vue/runtime core -D将嵌套模块添加为顶级依赖项。(您必须确保@vue/runtime内核的版本与项目中安装的vue版本相匹配。)(不推荐)使用0.18.5版本的unplugin-vue-components组件,而不是最新版本。(之所以有效,是因为在此版本之前,unplugin-vue-components 组件将components.d.ts中的模块声明为“vue”。缺点是,您将错过插件的最新更新和改进。)(不建议)手动更新components.d.ts中的模块声明名称,以声明模块“vue”,而不是声明模块“@vue/runtime core”(这很不方便,因为每当取消插入vue组件自动生成新的components.d.ts文件并覆盖您的更改时,您都必须更新模块名称。)注意:如果您选择了选项1或2并创建了.npmrc文件,请在之后运行pnpm i以使用最新的配置更新node_modules。然后,重新加载工作区。自动导入组件的Intellisense应再次工作。如果这么操作还是不行,就重启下vscode就ok啦祝福即将接近2022年除夕啦,小编在这里祝福大家在新的一年里,新年快乐,心想事成,万事如意,代码永无bugger
一、引言MongoDB 是一个由 C++ 语言编写的基于分布式文件存储的数据库,MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。常用用于物流场景-地理位置信息存储、社交场景-储存储用户信息、物联网场景-监控数据、日志记录等,MongoDB在这些场景的应用比其他数据库有这巨大优势。二、下载MongoDB1、检查CentOS是否已安装过Mongodb:2、查看CentOS版本 cat /etc/redhat-release3、去到Mongodb官网,选择对应版本下载①:去到官网下载地址:https://www.mongodb.com/try/download/community②:选择对应版本直接下载或者选择“Copy Link”获取下载地址:三、CentOS 安装MongoDB方法1、去到MongoDB安装目录,下载MongoDB安装包:wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-4.4.13.tgz2、解压MongoDB安装包: tar -zxvf mongodb-linux-x86_64-rhel80-4.4.13.tgz3、重命名解压后的MongoDB文件名: mv mongodb-linux-x86_64-rhel80-4.4.13 mongodb4、在MongoDB文件夹再里创建二个文件夹:mkdir data //用来存放数据库数据5、进入MongoDB文件下面的bin目录创建配置文件: vi mongod.conf dbpath=/usr/local/mongodb/data logpath=/usr/local/mongodb/logs/mongodb.log6、配置MongoDB环境变量export MONGODB_HOME=/usr/local/mongodb export PATH=$PATH:$MONGODB_HOME/bin7、启动MongoDB,在bin目录下执行启动命令:mongod -f /usr/local/MongoDB/mongod.conf出现successfully即证明服务成功启动!或者用ps aux | grep mongod查看服务是否运行8、创建一个对数据库test具有读写权限的用户 roles:[{role:"readWrite",db:"test"}]本地连接远程ecs 安全组配置总共三步,图解如下:配置完这个安全组后,你还是访问不了,气不气。那是因为防火墙还没有配置呢?温馨提示:腾讯云的是在防火墙设置哈,而不是安全组哦!!!防火墙设置防火墙貌似可以设置一个范围的,但是个人觉得还是一个一个端口配置比较安全吧。在这里可以使用命令,也可以服务器安装宝塔来进行配置宝塔宝塔的安装教程请自己百度。下面演示如何配置命令来配置firewall-cmd --zone=public --add-port=9001/tcp --permanent: 配置 9001 端口,效果如下:FirewallD is not running,经过排查发现是防火墙就没打开,新买的服务器防火墙默认没有开启。开启防火墙检查防火墙有没有开启的命令如下: systemctl status firewalld开启防火墙的命令如下:systemctl start firewalld,效果:开启防火墙又报错了,咋们就继续百度,看看怎么解决。原来防火墙默认是锁定的,那么需要使用命令来取消服务的锁定命令如下: systemctl unmask firewalld最后防火墙开启了,端口还没有打开呢?firewall-cmd --zone=public --query-port=9001/tcp 查询端口是否打开firewall-cmd --zone=public --add-port=9001/tcp --permanent 开放对应端口firewall-cmd --list-ports 查看已经开启的防火墙端口firewall-cmd --reload 重启防火墙开了新的防火墙一定要重启防火墙,不然不生效ps: 这里在介绍一下如何开启防火墙范围:vim /etc/firewalld/zones/public.xml
引言<<往期回顾>>1.vue3源码分析——rollup打包monorepo2.vue3源码分析——实现组件的挂载流程本期来实现,setup里面使用props,父子组件通信props和emit等,所有的源码请查看本期的内容与上一期的代码具有联动性,所以需要明白本期的内容,最后是先看下上期的内容哦!😃😃😃实现render中的this在render函数中,可以通过this,来访问setup返回的内容,还可以访问this.$el等测试用例由于是测试dom,jest需要提前注入下面的内容,让document里面有app节点,下面测试用例类似在html中定义一个app节点哦 let appElement: Element; beforeEach(() => { appElement = document.createElement('div'); appElement.id = 'app'; document.body.appendChild(appElement); afterEach(() => { document.body.innerHTML = ''; 复制代码本功能的测试用例正式开始test('实现代理对象,通过this来访问', () => { let that; const app = createApp({ render() { // 在这里可以通过this来访问 that = this; return h('div', { class: 'container' }, this.name); setup() { return { name: '123' const appDoc = document.querySelector('#app') app.mount(appDoc); // 绑定值后的html expect(document.body.innerHTML).toBe('<div id="app"><div class="container">123</div></div>'); const elDom = document.querySelector('#container') // el就是当前组件的真实dom expect(that.$el).toBe(elDom); 复制代码分析上面的测试用例1.setup返回是对象的时候,绑定到render的this上面2.$el则是获取的是当前组件的真实dom解决这两个需求:1.需要在render调用的时候,改变当前函数的this指向,但是需要思考的一个问题是:this是啥,它既要存在setup,也要存在el,咋们是不是可以用一个proxy来绑定呢?在哪里创建呢 可以在处理组件状态setupStatefulComponent来完成改操作2.el则是在mountElement中挂载真实dom的时候,把当前的真实dom绑定在vnode当中编码针对上面的分析,需要在setupStatefulComponent中来创建proxy并且绑定到instance当中,并且setup的执行结果如果是对象,也已经存在instance中了,可以通过instance.setupState来进行获取function setupStatefulComponent(instance: any) { instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] // ...省略其他 // 然后在setupRenderEffect调用render的时候,改变当前的this执行,执行为instance.proxy function setupRenderEffect(instance: any, vnode: any, container: any) { // 获取到vnode的子组件,传入proxy进去 const { proxy } = instance const subtree = instance.render.call(proxy) // ...省略其他 复制代码通过上面的操作,从render中this.xxx获取setup返回对象的内容就ok了,接下来处理el需要在mountElement中,创建节点的时候,在vnode中绑定下,el,并且在setupStatefulComponent 中的代理对象中判断当前的key// 代理对象进行修改 instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] }else if(key === '$el'){ return instance.vnode.el // mount中需要在vnode中绑定el function mountElement(vnode: any, container: any) { // 创建元素 const el = document.createElement(vnode.type) // 设置vnode的el vnode.el = el //…… 省略其他 复制代码看似没有问题吧,但是实际上是有问题的,请仔细思考一下,mountElement是不是比setupStatefulComponent 后执行,setupStatefulComponent执行的时候,vnode.el不存在,后续mountelement的时候,vnode就会有值,那么上面的测试用例肯定是报错的,$el为null解决这个问题的关键,mountElement的加载顺序是 render -> patch -> mountElement,并且render函数返回的subtree是一个vnode,改vnode中上面是mount的时候,已经赋值好了el,所以在patch后执行下操作 function setupRenderEffect(instance: any, vnode: any, container: any) { // 获取到vnode的子组件,传入proxy进去 const { proxy } = instance const subtree = instance.render.call(proxy) patch(subtree, container) // 赋值vnode.el,上面执行render的时候,vnode.el是null vnode.el = subtree.el 复制代码至此,上面的测试用例就能ok通过啦!实现on+Event注册事件在vue中,可以使用onEvent来写事件,那么这个功能是怎么实现的呢,咋们一起来看看测试用例 test('测试on绑定事件', () => { let count = 0 console.log = jest.fn() const app = createApp({ render() { return h('div', { class: 'container', onClick() { console.log('click') count++ onFocus() { count-- console.log(1) }, '123'); const appDoc = document.querySelector('#app') app.mount(appDoc); const container = document.querySelector('.container') as HTMLElement; // 调用click事件 container.click(); expect(console.log).toHaveBeenCalledTimes(1) // 调用focus事件 container.focus(); expect(count).toBe(0) expect(console.log).toHaveBeenCalledTimes(2) 复制代码分析在本功能的测试用例中,可以分析以下内容:1.onEvent事件是在props中定义的2.事件的格式必须是 on + Event的格式解决问题:这个功能比较简单,在处理prop中做个判断, 属性是否满足 /^on[A-Z]/i这个格式,如果是这个格式,则进行事件注册,但是vue3会做事件缓存,这个是怎么做到?缓存也好实现,在传入当前的el中增加一个属性 el._vei || (el._vei = {}) 存在这里,则直接使用,不能存在则创建并且存入缓存编码在mountElement中增加处理事件的逻辑 const { props } = vnode for (let key in props) { // 判断key是否是on + 事件命,满足条件需要注册事件 const isOn = (p: string) => p.match(/^on[A-Z]/i) if (isOn(key)) { // 注册事件 el.addEventListener(key.slice(2).toLowerCase(), props[key]) // ... 其他逻辑 el.setAttribute(key, props[key]) 复制代码事件处理就ok啦父子组件通信——props父子组件通信,在vue中是非常常见的,这里主要实现props与emit测试用例 test('测试组件传递props', () => { let tempProps; console.warn = jest.fn() const Foo = { name: 'Foo', render() { // 2. 组件render里面可以直接使用props里面的值 return h('div', { class: 'foo' }, this.count); setup(props) { // 1. 此处可以拿到props tempProps = props; // 3. readonly props props.count++ const app = createApp({ name: 'App', render() { return h('div', { class: 'container', h(Foo, { count: 1 }), h('span', { class: 'span' }, '123') const appDoc = document.querySelector('#app') app.mount(appDoc); // 验证功能1 expect(tempProps.count).toBe(1) // 验证功能3,修改setup内部的props需要报错 expect(console.warn).toBeCalled() expect(tempProps.count).toBe(1) // 验证功能2,在render中可以直接使用this来访问props里面的内部属性 expect(document.body.innerHTML).toBe(`<div id="app"><div class="container"><div class="foo">1</div><span class="span">123</span></div></div>`) 复制代码分析根据上面的测试用例,分析props的以下内容:1.父组件传递的参数,可以给到子组件的setup的第一个参数里面2.在子组件的render函数中,可以使用this来访问props的值3.在子组件中修改props会报错,不允许修改解决问题:问题1: 想要在子组件的setup函数中第一个参数,使用props,那么在setup函数调用的时候,把当前组件的props传入到setup函数中即可 问题2: render中this想要问题,则在上面的那个代理中,在加入一个判断,key是否在当前instance的props中 问题3: 修改报错,那就是只能读,可以使用以前实现的api shallowReadonly来包裹一下既可编码1. 在setup函数调用的时候,传入instance.props之前,需要在实例上挂载props export function setupComponent(instance) { // 获取props和children const { props } = instance.vnode // 处理props instance.props = props || {} // ……省略其他 //2. 在setup中进行调用时作为参数赋值 function setupStatefulComponent(instance: any) { // ……省略其他 // 获取组件的setup const { setup } = Component; if (setup) { // 执行setup,并且获取到setup的结果,把props使用shallowReadonly进行包裹,则是只读,不能修改 const setupResult = setup(shallowReadonly(instance.props)); // …… 省略其他 // 3. 在propxy中在加入判断 instance.proxy = new Proxy({}, { get(target, key){ // 判断当前的key是否存在于instance.setupState当中 if(key in instance.setupState){ return instance.setupState[key] }else if(key in instance.props){ return instance.props[key] }else if(key === '$el'){ return instance.vnode.el 复制代码做完之后,可以发现咋们的测试用例是运行没有毛病的😃😃😃组件通信——emit上面实现了props,那么emit也是少不了的,那么接下来就来实现下emit测试用例test('测试组件emit', () => { let count; const Foo = { name: 'Foo', render() { return h('div', { class: 'foo' }, this.count); setup(props, { emit }) { // 1. setup对象的第二个参数里面,可以结构出emit,并且是一个函数 // 2. emit 函数可以父组件传过来的事件 emit('click') // 验证emit1,可以执行父组件的函数 expect(count.value).toBe(2) // 3 emit 可以传递参数 emit('clickNum', 5) // 验证emit传入参数 expect(count.value).toBe(7) // 4 emit 可以使用—的模式 emit('click-num', -5) expect(count.value).toBe(2) const app = createApp({ name: 'App', render() { return h('div', {}, [ h(Foo, { onClick: this.click, onClickNum: this.clickNum, count: this.count }) setup() { const click = () => { count.value++ count = ref(1) const clickNum = (num) => { count.value = Number(count.value) + Number(num) return { click, clickNum, count const appDoc = document.querySelector('#app') app.mount(appDoc); // 验证挂载 expect(document.body.innerHTML).toBe(`<div id="app"><div><div class="foo">1</div></div></div>`) 复制代码分析根据上面的测试用例,可以分析出:1.emit 的参数是在父组件的props里面,并且是以 on + Event的形式2.emit 作为setup的第二个参数,并且可以结构出来使用3.emit 函数里面是触发事件的,事件名称,事件名称可以是小写,或者是 xxx-xxx的形式4.emit 函数的后续可以传入多个参数,作为父组件callback的参数解决办法: 问题1: emit 是setup的第二个参数,那么可以在setup函数调用的时候,传入第二个参数 问题2: 关于emit的第一个参数,可以做条件判断,把xxx-xxx的形式转成xxxXxx的形式,然后加入on,最后在props中取找,存在则调用,不存在则不调用 问题3:emit的第二个参数,则使用剩余参数即可编码// 1. 在setup函数执行的时候,传入第二个参数 const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit }); // 2. 在setup中传入第二个参数的时候,还需要在实例上添加emit属性哦 export function createComponentInstance(vnode) { const instance = { // ……其他属性 // emit函数 emit: () => { }, instance.emit = emit.bind(null, instance); function emit(instance, event, ...args) { const { props } = instance // 判断props里面是否有对应的事件,有的话执行,没有就不执行,处理emit的内容,详情请查看源码 const key = handlerName(capitalize(camize(event))) const handler = props[key] handler && handler(...args) return instance 复制代码到此就圆满成功啦!🎉🎉🎉
引言<<往期回顾>>1.手写vue3源码——创建项目2.手写vue3源码——reactive, effect ,scheduler, stop3.手写vue3源码——readonly, isReactive,isReadonly, shallowReadonly4.手写vue3源码——ref, computed5.vue3源码分析——rollup打包monorepo接下来一起学习下,runtime-core里面的方法,本期主要实现的内容是,通过createApp方法,到mount最后把咋们的dom给挂载成功!,所有的源码请查看效果咋们需要使这个测试用例跑成功!,在图中可以发现,调用app传入了一个render函数,然后挂载,对比期望结果!测试dom思考再三,先把这一节先说了,jest是怎么来测试dom的?jest默认的环境是node,在jest.config.js中可以看到npm有在node中实现了浏览器环境的api的库,jsdom、happy-dom 等,咋们这里就使用比较轻的happy-dom,但是happy-dom里面与jest结合是一个子包——@happy-dom/jest-environment,那就安装一下pnpm add @happy-dom/jest-environment -w -Dpnpm add @happy-dom/jest-environment -w -D 复制代码由于我项目示例使用的是monorepo,所以只需要在runtime-core中进行以下操作即可:在jest.config.js中修改环境 testEnvironment: '@happy-dom/jest-environment', 复制代码然后你就可以在当前子包中使用正确运行测试用例了。小问题1.全局的package.json运行的时候报错,内容是没有dom环境2.vscode 插件 jest自动运行失败针对第一个问题,在上一节vue3源码分析——rollup打包monorepo中我们可以知道,在全局可以执行packages中的每一个脚本,同理,我们做以下操作:// 在全局的package.json中的test修改成这句话 "test": "pnpm -r --filter=./packages/** run test", 复制代码那么就可以执行啦!第二个问题,这个是vscode的插件问题,我们可以重jest插件的文档入手,可以发现jest执行的时候,可以自定义脚本,解决办法如下:意思是说,jest自动执行的时候,直接执行我们项目的test脚本,由于第一个问题的解决,第二个问题也是ok的哦!🎉🎊正文在正文之前,希望您先看过本系列文章的 vue3 组件初始化流程,这里详细介绍了组件的初始化流程,这里主要是实现挂载测试用例describe('apiCreateApp', () => { // 定义一个跟节点 let appElement: Element; // 开始之前创建dom元素 beforeEach(() => { appElement = document.createElement('div'); appElement.id = 'app'; document.body.appendChild(appElement); // 执行完测试后,情况html内部的内容 afterEach(() => { document.body.innerHTML = ''; test('测试createApp,是否正确挂载', () => { // 调用app方法,传入render函数 const app = createApp({ render() { return h('div', {}, '123'); const appDoc = document.querySelector('#app') // 调用mount函数 app.mount(appDoc); expect(document.body.innerHTML).toBe('<div id="app"><div>123</div></div>'); 复制代码流程图1.一开始需要createApp,那咋们就给一个,并且返回一个mount函数function createApp(rootComponent) { const app = { _component: rootComponent, mount(container) { const vnode = createVNode(rootComponent); render(vnode, container); return app; 复制代码2.mount内部需要创建vnode的方法,咋们也给一个,并且把跟组件作为参数传入function createVNode(type, props, children) { // 一开始咋们就是这么简单,vnode里面有一个type,props,children这几个关键的函数 const vnode = { type, props: props || {}, children: children || [] return vnode; 复制代码3.需要render函数,咋们也来创建一个,并且内容只调用了patch,咋把这两个一起创建function render(vnode, container) { patch(vnode, container); function patch(vnode, container) { // patch需要判断vnode的type,如果是对象,则是处理组件,如果是字符串div,p等,则是处理元素 if (isObj(vnode.type)) { processComponent(null, vnode, container); } else if (String(vnode.type).length > 0) { processElement(null, vnode, container); 复制代码4.咋们先处理组件吧,创建一个processComponent函数// n1 是老节点,n2则是新节点,container是挂载的容器 function processComponent(n1, n2, container) { // 如果n1不存在,直接是挂载组件 if (!n1) { mountComponent(n2, container); 复制代码5.创建mountComponent方法来挂载组件function mountComponent(vnode, container) { // 创建组件实例 const instance = createComponentInstance(vnode); // 处理组件,初始化setup,slot,props, render等在实例的挂载 setupComponent(instance); // 执行render函数 setupRenderEffect(instance, vnode, container); 复制代码6.创建组件的实例createComponentInstance// 是不是组件实例很简单,就只有一个vnode,props, function createComponentInstance(vnode) { const instance = { vnode, props: {}, type: vnode.type return instance; 复制代码7.处理组件的状态, 这个函数里面会比较多内容function setupComponent(instance) { const { props } = instance; // 初始化props initProps(instance, props); // 处理组件的render函数 setupStatefulComponent(instance); function setupStatefulComponent(instance) { const Component = instance.type; const { setup } = Component; // 是否存在setup if (setup) { const setupResult = setup(); // 处理setup的结果 handleSetupResult(instance, setupResult); // 完成render在instance中 finishComponentSetup(instance); function handleSetupResult(instance, setupResult) { // 函数作为instance的render函数 if (isFunction(setupResult)) { instance.render = setupResult; } else if (isObj(setupResult)) { instance.setupState = proxyRefs(setupResult); finishComponentSetup(instance); function finishComponentSetup(instance) { const Component = instance.type; // 如果没有的话,直接使用Component的render if (!instance.render) { instance.render = Component.render; 复制代码8.创建setupRenderEffect,执行实例的render函数function setupRenderEffect(instance, vnode, container) { const subtree = instance.render(); patch(subtree, container); 复制代码9.处理完组件,接下来该处理元素了 processElement// 这个方法和processComponent一样 function processElement(n1, n2, container) { // 需要判断是更新还是挂载 if (n1) ; else { mountElement(n2, container); 复制代码10.挂载元素 mountElementfunction mountElement(vnode, container) { // 创建根节点 const el = document.createElement(vnode.type); const { props } = vnode; // 挂载属性 for (let key in props) { el.setAttribute(key, props[key]); const children = vnode.children; // 如果children是数组,继续patch if (Array.isArray(children)) { children.forEach((child) => { patch(child, el); } else if (String(children).length > 0) { el.innerHTML = children; // 把元素挂载到根节点 container.appendChild(el); 复制代码恭喜,到这儿就完成本期的内容,重头看一下,vue组件的挂载分为两种,处理组件和处理元素,最终回归到处理元素上面,最后实现节点的挂载,该内容是经过非常多删减,只是为了实现一个基本挂载,还有许多的边界都没有完善,后续继续加油🐱👓🐱👓🐱👓
引言<<往期回顾>>1.手写vue3源码——创建项目2.手写vue3源码——reactive, effect ,scheduler, stop3.手写vue3源码——readonly, isReactive,isReadonly, shallowReadonly4.手写vue3源码——ref, computed本期咋们就先放一放源码,咋们如何打包monorepo应用,主要是源码看累了🤣🤣🤣,打包工具也是一门必须课,所有的源码请查看效果为了提供大家的学习兴趣,咋们先来看看效果,准备发车,请系好安全带🚗🚗🚗cjs 结果预览esm 结果预览声明文件预览正文vue3使用的是rollup来打包的,咋们也用rollup来打包咋们的应用,有不了解rollup的请查看官网,monorepo是多个单体仓库合并得到的,那么咋们就先来打包单个仓库,然后再来想办法怎么一键打包全部打包shared在我项目中,shared仓库是相当与utils函数的集合,用于对外导出一些工具函数,那么咋们可以在本目录下的package.json中安装rollup。 正当我就想在shared目录下面安装rollup插件的时候,我大脑给了个慢着的问号?monorepo 是不是可以在跟下面安装依赖,然后子包都可以共享,基于这一特征。我毫不犹豫在根目录下面敲下了下面的命令:pnpm add rollup -w -D 复制代码有了rollup,咋们是不是需要在打包的目录下面来搞个配置文件rollup.config.js,里面咋们写上入口,出口,打包的格式等// 由于咋们需要打包成cjs, ems的格式,对外导出一个函数吧 input: './src/index.ts', output: { file: 'dist/index.esm.js', format: 'esm', input: './src/index.ts', output: { file: 'dist/index.cjs.js', format: 'cjs', 复制代码然后在本目录下的package.json中加入打包的命令: "build": "rollup -c" 复制代码nice, 到这了就完了,咋们试一下,结果:分析错误可以发现,咋们是用了ts的语法,rollup无法转换ts的语法,需要使用插件了。😉😉😉那么rollup转换ts的插件也是有好多种,这里咋们用一个最快的那种,esbuild, rollup-plugin-esbuildpnpm add esbuild rollup-plugin-esbuild -w -D 复制代码关于rollup-plugin-esbuild这个插件,官方的介绍是说:esbuild is by far one of the fastest TS/ESNext to ES6 compilers and minifier, this plugin replaces rollup-plugin-typescript2, @rollup/plugin-typescript and rollup-plugin-terser for you. 意思是说,这个插件是目前来说转换ts/esnext到es6是最快的编译和压缩,这个插件可以代替 rollup-plugin-typescript2, @rollup/plugin-typescript and rollup-plugin-terser的集合但是如果咋们需要打包非常低版本的代码,那就请查看rollup 实战第三节 打包生产打包低版本的代码.言归正传,那么咋们把插件用上,在配置文件上加上插件//... 省略其他 plugins: [ esbuild({ target: 'node14', 复制代码再来一次🤩🤩🤩通过结果,咋们可以看到已经打包成功了!🎉🎉🎉但是咋们是有ts的,肯定还需要生成咋们代码的类型吧,那就使用 rollup-plugin-dts这个来生成pnpm add rollup-plugin-dts -w -D 复制代码rollup-plugin-dts详情请查看// 在数组后面在加上一项, input: './src/index.ts', output: { file: 'dist/index.dts', format: 'esm', plugins: [ dts(), 复制代码然后就可以ok啦,咋们单个项目就完成了打包多个既然单个是这么写,那么其他的咋们是不是也可以写配置文件呢?对的,没错,可以在对应的单体项目下面写上rollup.config.js来对他们进行打包的配置然后咋们在跟目录下面的package.json中加入一行命令:"build": "pnpm -r --filter=./packages/** run build" 复制代码咋们来拆分下命令1.pnpm -r 等同于 pnpm --recursive,意思是说在工作区的每个项目中运行命令,不包括根项目,详情查看2.--filter=./packages/**意思是说,过滤其他文件和文件夹,只使用packages下面的所有文件夹3.run build 是 pnpm -r run build的后缀,执行package.json中的build指令,详情请查看合起来的意思是说,依次执行packages里面所有文件夹的package.json的build命令优化通过上面的方式咋们就可以打包成功了,但是这里咋们还可以进行优化下,每一次打包dist结果都需要手动删除,咋们可以使用 rimraf 这个库来帮我们自动删除pnpm add rimraf -d -W 复制代码然后在每一个子包中修改build的命令"build": "rimraf dist && rollup -c" 复制代码对比vue3打包这里可能有的人会说,vue3仓库都不是这么玩的,的确,vue3仓库的打包流程如下:有兴趣的可以取看源码哈,这里给出流程图,想要使用这种方式的就自己实现哈!🎃🎃🎃
在上一篇文章中,咋们介绍了vue3组件的初始化流程,接下来咋们来一起分析下vue3组件的更新流程是咋样的先写一个组件,App.js, 然后咋们来执行更新的流程import { h, ref } from "vue"; export default { name: 'App', setup() { const count = ref(0); // 把count赋值给window,然后在控制台中来改变数据,看看流程是咋样变化的 window.count = count return { count render() { return h('div', { pId: '"helloWorld"' }, [ h('p', {}, 'hello world'), h('p', {}, `count的值是: ${this.count}`), 复制代码mountmount阶段就是上篇文章,这里直接跳过,咋们来走更新流程update还记得组件挂载阶段中的 setupRenderEffect么? 在这里的时候会进行依赖收集,会在实例instance上挂载一个方法instance.update = effect(componentUpdateFn, { scheduler: () => { queueJob(instance.update); 复制代码当数据发送变化的时候,就会触发 componentUpdateFn函数, 不清楚响应式系统的可以查看这里整体的流程图如下:1.第一步肯定就是执行 componentUpdateFn,由于组件已经挂载完成,直接走更新操作2.判断属性是否有变化,如果有变化的话需要更新属性,咋们这里没有属性发生变化,直接调用normalizeVNode(instance.render.call(proxyToUse, proxyToUse))获取children3.触发 beforeUpdated hook4.传入参数,调用patch,后续的流程是根据咋代码的修改count内容来走的5.根据参数,进入path的的 processElement6.更新流程,直接调用 updateElement7.更新属性8.更新children (diff算法)属性更新咋们来分析下 vue3 中属性变化的情况第一种情况 属性增加let oldProps = {a: 1} let newProps = {a:1,b:2} 复制代码对于这种情况,咋们怎么才能找出属性的变化,是不是就是应该遍历 newProps 如果里面的key 在 oldProps 中不存在,则标记为新增的属性 代码应该这么写:for (const key in newProps) { const prevProp = oldProps[key]; const nextProp = newProps[key]; if (prevProp !== nextProp) { // 新增属性 复制代码第二种情况 属性减少let oldProps = {a: 1, c: 4} let newProps = {a:1} 复制代码对于这种情况,咋们要找出属性的变化,直接遍历 oldProps 既即可,和上面的方式是一样的第三种情况 属性变化let oldProps = {a: 2} let newProps = {a:1} 复制代码对于这种情况,咋们是不是还需要一个 对比属性的函数来,循环遍历依次来对比属性的变化呢?针对上面的情况一和情况二,都可以用同一个方法来新增,修改,删除属性,vue3 只不过把处理的都映射给每一个dom了/** * @param el 更新的真实dom * @param key 属性的key * @param preValue 旧的值 * @param nextValue 新的值 function hostPatchProp(el, key, preValue, nextValue){ // 传入的key,是不是事件处理函数 if (isOn(key)) { // 添加事件处理函数的时候需要注意一下 // 1. 添加的和删除的必须是一个函数,不然的话 删除不掉 // 那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到 // 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次 // 缓存所有的事件函数 const invokers = el._vei || (el._vei = {}); const existingInvoker = invokers[key]; // 属性存在,直接修改 if (nextValue && existingInvoker) { existingInvoker.value = nextValue; } else { // 属性不存在,进行新增或者删除事件 const eventName = key.slice(2).toLowerCase(); // 注册事件 if (nextValue) { const invoker = (invokers[key] = nextValue); el.addEventListener(eventName, invoker); } else { // 移除事件 el.removeEventListener(eventName, existingInvoker); invokers[key] = undefined; } else { // 新的值不存在,直接删除操作 if (nextValue === null || nextValue === "") { el.removeAttribute(key); } else { // 反之存在则进行添加新的属性 el.setAttribute(key, nextValue); 复制代码更新children更新children,这里有一个条件,如果新的children和old children 则触发diff 算法,其实diff 算法也没有想象中的那么复杂,是一点点根据边界情况和性能优化写出来的,下面咋们就一起来写一个简单版的diff算法吧在处理 children 更新的过程中,采用的是一种双端对比的模式,这样就可以缩小对比的范围左侧对比通过左侧对比获取起始位置/** * 是否相同 * @param {*} n1 * @param {*} n2 * @returns const isSame = (n1, n2) => { return n1.value === n2.value && n1.key === n2.key // 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }] const n2 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'E', key: 'E' }, { value: 'D', key: 'D' }] // 从左侧开始查找,看看左侧有哪些是相同的,那么在更新的时候就可以跳过相同的节点,节约性能 const diff = (n1, n2) => { // 为了方便演示,咋们就只操作 n1来完成diff的操作 const copyN1 = JSON.parse(JSON.stringify(n1)) let i = 0; let e1 = n1.length - 1 let e2 = n2.length - 1 // 确定起始的位置i while (i <= e1 && i <= e2) { if (isSame(n1[i], n2[i])) { i++ } else { break 复制代码从上面的代码,咋们可以获取到i的值,起始位置就获取好了右侧对比通过右侧对比,获取结束位置,用来锁定中间有问题的部分// 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }] const n2 = [{ value: 'D', key: 'D' }, { value: 'E', key: 'E' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }] // 上面咋们知道了,左侧id的位置,那么接下来咋们来确定右侧的位置 while (i <= e1 && i <= e2) { if (isSame(n1[e1], n2[e2])) { } else { break 复制代码这样咋们就确定了结束位置了,接下来就是判断边界条件了新的比老的长———创建新的在新的比老的长里面,分为两种情况,1.新的右边比老的长2.新的左边比老的长右边比老的长// 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] const n2 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }] // ... 获取i e1, e2 // 在本种情况种, i = 2, e1 = 1 , e2 = 2 // 当 i > e1 时候,并且 i <= e2 的时候,咋们就可以确定新节点的右侧比老节点长 if (i > e1 && i <= e2) { // 增加新的节点i copyN1.splice(i, 0, ...n2.slice(i)) return copyN1 复制代码左边比老的长// 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] const n2 = [ { value: 'C', key: 'C' },{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] // ... 省略其他逻辑 // 在这种情况下, i = 0, e1 = -1, e2 = 0, 所以条件还是 i > e1 && i <= e2, // 但是上面的 copyN1.splice(i, 0, ...n2.slice(i)) 这个方法是否适合这里呢,显然不适合 if (i > e1 && i <= e2) { while (i <= e2) { // 增加新的节点i,这里与dom操作是不一样的,在dom种没有插入指定位置的api, copyN1.splice(i, 0, n2[i]) i++ 复制代码新的比老的短———删除老的在新的比老的短里面,分为两种情况,1.新的右边比老的短2.新的左边比老的短新的右边比老的短// 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }] const n2 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] // ... 省略其他逻辑 // 在这种情况种,咋们的 i = 2, e1 = 2 , e2 = 1 所以满足新节点比老节点短的条件是 i <= e1 && i > e2 else if (i <= e1 && i > e2) { // 新的节点比老的节点短,进行删除老的节点 while (i <= e1) { copyN1.splice(i, 1) i++ 复制代码新的左边比老的短// 咋们的新老节点分别为n1, n2 const n1 = [{ value: 'C', key: 'C' },{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] const n2 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }] // ... 省略其他逻辑 // 在这种情况种,咋们的 i = 2, e1 = 2 , e2 = 1 所以满足新节点比老节点短的条件是 i <= e1 && i > e2, 这里会发现和我们右侧的是一样的 else if (i <= e1 && i > e2) { // 新的节点比老的节点短,进行删除老的节点 while (i <= e1) { copyN1.splice(i, 1) i++ 复制代码中间对比通过上面的左右对比,咋们就可以得出一个新的区域对于n2的范围在 【i,e2】 而n1的范围是【i, e1】 在中间对比的时候咋们有一种很直接的方法—— 直接双重for循环来暴力破解😀😀😀,但是这么做肯定是有点费性能的,vue3肯定不是这么做的,人家在里面用了个 最长递增子序列算法来查找尽可能多的节点是不用变化的. 不熟悉最长递增子序列算法请参考这里在比较中间部分的时候,又会有以下几种情况:1.剩余部分的节点都存在于老的和新的,只是顺序发生变化2.剩余部分只存在于新的,需要增加节点3.剩余部分只存在于老的,需要删除节点中间部分只存在于老的————删除// 咋们的新老节点分别为n1, n2 const n51 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'D', key: 'D' }, { value: 'E', key: 'E' }] const n52 = [{ value: 'A', key: 'A' },{ value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'E', key: 'E' }] // 在这里咋们可以看到,老节点中间是多了一个D节点,那咋们就需要把这个节点找出来 ... 省略其其他逻辑 else { //处理中间节点 let s1 = i, s2 = i; // 对新节点建立索引,给缓存起来, const keyToNewIndexMap = new Map(); // 缓存新几点 for (let i = s2; i <= e2; i++) { keyToNewIndexMap.set(n2[i].key, i) // 需要处理新节点的数量 const toBePatched = e2 - s2; // 遍历老节点,需要把老节点有的,而新节点没有的给删除 for (let i = s1; i <= e1; i++) { let newIndex; // 存在key,从缓存中取出新节点的索引 if (n1[i].key && n1[i].key == null) { newIndex = keyToNewIndexMap.get(n1[i].key) } else { // 不存在key,遍历新节点,看看能不能在新节点中找到老节点 for (let j = s2; j <= e2; j++) { if (isSame(n1[i], n2[j])) { newIndex = j break // 如果newIndex 不存在,则是老节点中有的,而新节点没有,删除 if (newIndex === undefined) { copyN1.splice(i, 1) 复制代码在这里咋们可以看错,在v-for的时候,key的作用了吧😄😄😄,不写的话就会再来一遍循环,造成性能的浪费。中间部分的节点新节点有,老节点无————新增节点// 咋们的新老节点分别为n1, n2 const n51 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'E', key: 'E' }] const n52 = [{ value: 'A', key: 'A' },{ value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'D', key: 'D' },{ value: 'E', key: 'E' }] // 省略其他逻辑 // 在这里咋们是知道D节点是新增的节点,为了让代码知道D节点是新增的节点,咋们需要做一个新老节点的映射 // 对老节点建立索引映射, 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在 const newIndexToOldIndexMap = new Array(toBePatched).fill(0) 在newIndex 存在的时候,来更新老节点的 // 把老节点的索引记录下来, +1 的原因是怕,i 恰好为0 newIndexToOldIndexMap[newIndex - s2] = i + 1 // 遍历新节点, for (let i = s2; i <= toBePatched; i++) { // 如果新节点在老节点中不存在,则创建 if (newIndexToOldIndexMap[i] === 0) { copyN1.splice(i + s2, 0, n2[i + s2]) 复制代码中间部分节点都存在,移动位置// 咋们的新老节点分别为n1, n2 const n71 = [{ value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'D', key: 'D' }, { value: 'E', key: 'E' }] const n72 = [{ value: 'A', key: 'A' }, { value: 'C', key: 'C' }, { value: 'D', key: 'D' }, , { value: 'B', key: 'B' }, { value: 'E', key: 'E' }] // 在这种情况下,节点C和节点D的位置是没有变化的,之哟节点B是变化了的,所以咋们只要移动节点B // 我们人知道需要移动节点B呢? 移动的条件: 如果从老节点的newIndex 一直都是升序的话,机不需要移动,反之则移动,使用最长子序列来规定最小的移动范围 const diff = (n1, n2) => { const copyN1 = JSON.parse(JSON.stringify(n1)) let i = 0; let e1 = n1.length - 1 let e2 = n2.length - 1 // 确定起始的位置i while (i <= e1 && i <= e2) { if (isSame(n1[i], n2[i])) { i++ } else { break // 确定结束位置 while (i <= e1 && i <= e2) { if (isSame(n1[e1], n2[e2])) { } else { break // 条件一, 新节点比老节点长 // 条件1.1 新节点的右侧比老节点长 // 当 i > e1 时候,并且 i <= e2 的时候,咋们就可以确定新节点的右侧比老节点长 if (i > e1 && i <= e2) { while (i <= e2) { // 增加新的节点i copyN1.splice(i, 0, n2[i]) i++ } else if (i <= e1 && i > e2) { // 新的节点比老的节点短,进行删除老的节点 while (i <= e1) { copyN1.splice(i, 1) i++ } else { //处理中间节点 let s1 = i, s2 = i; // 对新节点建立索引,给缓存起来, const keyToNewIndexMap = new Map(); // 是否需要移动 let moved = false; // 最大新节点索引 let maxNewIndexSoFar = 0; // 收集新节点的key for (let i = s2; i <= e2; i++) { keyToNewIndexMap.set(n2[i].key, i) // 需要处理新节点的数量 const toBePatched = e2 - s2 + 1; // 对老节点建立索引映射, 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在 const newIndexToOldIndexMap = new Array(toBePatched).fill(0) // 遍历老节点,需要把老节点有的,而新节点没有的给删除 for (let i = s1; i <= e1; i++) { let newIndex; // 存在key,从缓存中取出新节点的索引 if (n1[i].key && n1[i].key == null) { newIndex = keyToNewIndexMap.get(n1[i].key) } else { // 不存在key,遍历新节点,看看能不能在新节点中找到老节点 for (let j = s2; j <= e2; j++) { if (isSame(n1[i], n2[j])) { newIndex = j break // 如果newIndex 不存在,则是老节点中有的,而新节点没有,删除 if (newIndex === undefined) { copyN1.splice(i, 1) } else { // 老节点在新节点中存在 // 把老节点的索引记录下来, +1 的原因是怕,i 恰好为0 newIndexToOldIndexMap[newIndex - s2] = i + 1 // 新的 newIndex 如果一直是升序的话,那么就说明没有移动 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true // 利用最长递增子序列来优化移动逻辑 // 因为元素是升序的话,那么这些元素就是不需要移动的 // 而我们就可以通过最长递增子序列来获取到升序的列表 // 在移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动 const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []; // increasingNewIndexSequence 返回的是最长递增子序列的索引 let j = 0; // 遍历新节点, for (let i = 0; i < toBePatched; i++) { // 如果新节点在老节点中不存在,则创建 if (newIndexToOldIndexMap[i] === 0) { copyN1.splice(i + s2, 0, n2[i + s2]) } else if (moved) { // 新老节点都存在,需要进行移动位置 if (j > increasingNewIndexSequence.length - 1 || i !== increasingNewIndexSequence[j]) { // 先删掉节点,然后插入 即是移动 copyN1.splice(newIndexToOldIndexMap[i] - 1, 1) copyN1.splice(i + s2, 0, n2[i + s2]) } else { j++ 复制代码自此,整个diff算法的核心就在这里了,文章里面采用的是diff数组,而vue里面是diff的是真实的dom测试 const oldNode = [ { value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'C', key: 'C' }, { value: 'E', key: 'E' }, { value: 'F', key: 'F' }, { value: 'G', key: 'G' }] const newNode = [ { value: 'A', key: 'A' }, { value: 'B', key: 'B' }, { value: 'D', key: 'D' }, { value: 'C', key: 'C' }, { value: 'E', key: 'E' }, { value: 'F', key: 'F' }, { value: 'G', key: 'G' }] console.log('oldNode', oldNode, 'newNode', newNode, '新节点和老节点都存在,位置发生变化', diff(oldNode, newNode)) 复制代码更多详情,请查看源码
文件处理一直都是前端人的心头病,如何控制好文件大小,文件太大上传不了,文件下载时间太长,tcp直接给断开了😱😱😱等效果为了方便大家有意义的学习,这里就先放效果图,如果不满足直接返回就行,不浪费大家的时间。文件上传文件上传实现,分片上传,暂停上传,恢复上传,文件合并等文件下载为了方便测试,我上传了1个1g的大文件拿来下载,前端用的是流的方式来保存文件的,具体的可以看这个api TransformStream正文本项目的地址是: https://github.com/cll123456/deal-big-file 需要的自提上传请带着以下问题来阅读下面的文章1.如何计算文件的hash,怎么做计算hash是最快的2.文件分片的方式有哪些3.如何控制分片上传的http请求(控制并发),大文件的碎片太多,直接把网络打垮4.如何暂停上传5.如何恢复上传等计算文件hash在计算文件hash的方式,主要有以下几种: 分片全量计算hash、抽样计算hash。在这两种方式上,分别又可以使用web-work和浏览器空闲(requestIdleCallback)来实现.web-work有不明白的可以看这里: https://juejin.cn/post/7091068088975622175requestIdleCallback 有不明白的可以看这里: https://juejin.cn/post/7069597252473815053接下来咋们来计算文件的hash,计算文件的hash需要使用 spark-md5这个库,全量计算文件hashexport async function calcHashSync(file: File) { // 对文件进行分片,每一块文件都是分为2MB,这里可以自己来控制 const size = 2 * 1024 * 1024; let chunks: any[] = []; let cur = 0; while (cur < file.size) { chunks.push({ file: file.slice(cur, cur + size) }); cur += size; // 可以拿到当前计算到第几块文件的进度 let hashProgress = 0 return new Promise(resolve => { const spark = new SparkMD5.ArrayBuffer(); let count = 0; const loadNext = (index: number) => { const reader = new FileReader(); reader.readAsArrayBuffer(chunks[index].file); reader.onload = e => { // 累加器 不能依赖index, count++; // 增量计算md5 spark.append(e.target?.result as ArrayBuffer); if (count === chunks.length) { // 通知主线程,计算结束 hashProgress = 100; resolve({ hashValue: spark.end(), progress: hashProgress }); } else { // 每个区块计算结束,通知进度即可 hashProgress += 100 / chunks.length // 计算下一个 loadNext(count); // 启动 loadNext(0); }全量计算文件hash,在文件小的时候计算是很快的,但是在文件大的情况下,计算文件的hash就会非常慢,并且影响主进程哦🙄🙄🙄抽样计算文件hash抽样就是取文件的一部分来继续,原理如下:/** * 抽样计算hash值 大概是1G文件花费1S的时间 * 采用抽样hash的方式来计算hash * 我们在计算hash的时候,将超大文件以2M进行分割获得到另一个chunks数组, * 第一个元素(chunks[0])和最后一个元素(chunks[-1])我们全要了 * 其他的元素(chunks[1,2,3,4....])我们再次进行一个分割,这个时候的分割是一个超小的大小比如2kb,我们取* 每一个元素的头部,尾部,中间的2kb。 * 最终将它们组成一个新的文件,我们全量计算这个新的文件的hash值。 * @param file {File} * @returns export async function calcHashSample(file: File) { return new Promise(resolve => { const spark = new SparkMD5.ArrayBuffer(); const reader = new FileReader(); // 文件大小 const size = file.size; let offset = 2 * 1024 * 1024; let chunks = [file.slice(0, offset)]; // 前面2mb的数据 let cur = offset; while (cur < size) { // 最后一块全部加进来 if (cur + offset >= size) { chunks.push(file.slice(cur, cur + offset)); } else { // 中间的 前中后去两个字节 const mid = cur + offset / 2; const end = cur + offset; chunks.push(file.slice(cur, cur + 2)); chunks.push(file.slice(mid, mid + 2)); chunks.push(file.slice(end - 2, end)); // 前取两个字节 cur += offset; // 拼接 reader.readAsArrayBuffer(new Blob(chunks)); // 最后100K reader.onload = e => { spark.append(e.target?.result as ArrayBuffer); resolve({ hashValue: spark.end(), progress: 100 }); }这个设计是不是发现挺灵活的,真是个人才哇在这两个的基础上,咋们还可以分别使用web-worker和requestIdleCallback来实现,源代码在hereヾ(≧▽≦*)o这里把我电脑配置说一下,公司给我分的电脑配置比较lower, 8g内存的老机器。计算(3.3g文件的)hash的结果如下:结果很显然,全量无论怎么弄,都是比抽样的更慢。文件分片的方式这里可能大家会说,文件分片方式不就是等分吗,其实还可以根据网速上传的速度来实时调整分片的大小哦!const handleUpload1 = async (file:File) => { if (!file) return; const fileSize = file.size let offset = 2 * 1024 * 1024 let cur = 0 let count = 0 // 每一刻的大小需要保存起来,方便后台合并 const chunksSize = [0, 2 * 1024 * 1024] const obj = await calcHashSample(file) as { hashValue: string }; fileHash.value = obj.hashValue; //todo 判断文件是否存在存在则不需要上传,也就是秒传 while (cur < fileSize) { const chunk = file.slice(cur, cur + offset) cur += offset const chunkName = fileHash.value + "-" + count; const form = new FormData(); form.append("chunk", chunk); form.append("hash", chunkName); form.append("filename", file.name); form.append("fileHash", fileHash.value); form.append("size", chunk.size.toString()); let start = new Date().getTime() // todo 上传单个碎片 const now = new Date().getTime() const time = ((now - start) / 1000).toFixed(4) let rate = Number(time) / 10 // 速率有最大和最小 可以考虑更平滑的过滤 比如1/tan if (rate < 0.5) rate = 0.5 if (rate > 2) rate = 2 offset = parseInt((offset / rate).toString()) chunksSize.push(offset) count++ //todo 可以发送合并操作了 }🥉🥉🥉ATTENTION!!! 如果是这样上传的文件碎片,如果中途断开是无法续传的(每一刻的网速都是不一样的),除非每一次上传都把 chunksSize(分片的数组)保存起来哦控制http请求(控制并发)控制http的请求咋们可以换一种想法,是不是就是控制异步任务呢?/** * 异步控制池 - 异步控制器 * @param concurrency 最大并发次数 * @param iterable 异步控制的函数的参数 * @param iteratorFn 异步控制的函数 export async function* asyncPool<IN, OUT>(concurrency: number, iterable: ReadonlyArray<IN>, iteratorFn: (item: IN, iterable?: ReadonlyArray<IN>) => Promise<OUT>): AsyncIterableIterator<OUT> { // 传教set来保存promise const executing = new Set<Promise<IN>>(); // 消费函数 async function consume() { const [promise, value] = await Promise.race(executing) as unknown as [Promise<IN>, OUT]; executing.delete(promise); return value; // 遍历参数变量 for (const item of iterable) { const promise = (async () => await iteratorFn(item, iterable))().then( value => [promise, value] ) as Promise<IN>; executing.add(promise); // 超出最大限制,需要等待 if (executing.size >= concurrency) { yield await consume(); // 存在的时候继续消费promise while (executing.size) { yield await consume(); }暂停请求暂停请求,其实也很简单,在原生的XMLHttpRequest 里面有一个方法是 xhr?.abort(),在发送请求的同时,在发送请求的时候,咋们用一个数组给他装起来,然后就可以自己直接调用abort方法了。在封装request的时候,咋们要求传入一个requestList就好:export function request({ method = "post", data, onProgress = e => e, headers = {}, requestList }: IRequest) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.onprogress = onProgress // 发送请求 xhr.open(method, baseUrl + url); // 放入其他的参数 Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]) xhr.send(data); xhr.onreadystatechange = e => { // 请求是成功的 if (xhr.readyState === 4) { if (xhr.status === 200) { if (requestList) { // 成功后删除列表 const i = requestList.findIndex(req => req === xhr) requestList.splice(i, 1) // 获取服务响应的结构 const resp = JSON.parse(xhr.response); // 这个code是后台规定的,200是正确的响应,500是异常 if (resp.code === 200) { // 成功操作 resolve({ data: (e.target as any)?.response } else { reject('报错了 大哥') } else if (xhr.status === 500) { reject('报错了 大哥') // 存入请求 requestList?.push(xhr) }有了请求数组后,那么咋们想暂时直接遍历请求数组,调用 abort方法恢复上传恢复上传是判断有哪些碎片上已经存在的,存在的就不需要上传了,不存在的继续上传。所以咋们要一个接口,verify 传入文件的hash,文件名称,判断文件是否存在或者说是上传了多少。/** * 验证文件是否存在 * @param req * @param res async handleVerify(req: http.IncomingMessage, res: http.ServerResponse) { // 解析post请求数据 const data = await resolvePost(req) as { filename: string, hash: string } const { filename, hash } = data // 获取文件后缀名称 const ext = extractExt(filename) const filePath = path.resolve(this.UPLOAD_DIR, `${hash}${ext}`) // 文件是否存在 let uploaded = false let uploadedList: string[] = [] if (fse.existsSync(filePath)) { uploaded = true } else { // 文件没有完全上传完毕,但是可能存在部分切片上传完毕了 uploadedList = await getUploadedList(path.resolve(this.UPLOAD_DIR, hash)) res.end( JSON.stringify({ code: 200, uploaded, uploadedList // 过滤诡异的隐藏文件 }注意,这里还需要在每一次验证的时候需要去删除片段的最后几块文件,防止最后几块文件是不完全上传的残杂.合并文件合并文件很好理解,就是把所有的碎片进行合并,但是有一个地方需要注意的是,咋们不能把所有的文件都读到内存中进行合并,而是使用流的方式来进行合并,边读边写入文件。写入文件的时候需要保证顺序,不然文件可能就会损坏了。这一部分代码会比较多,感兴趣的同学可以看源码文件下载对于文件下载的话,后端其实很简单,就是返回一个流就行,如下:/** * 文件下载 * @param req * @param res async handleDownload(req: http.IncomingMessage, res: http.ServerResponse) { // 解析get请求参数 const resp: UrlWithParsedQuery = await resolveGet(req) // 获取文件名称 const filePath = path.resolve(this.UPLOAD_DIR, resp.query.filename as string) // 判断文件是否存在 if (fse.existsSync(filePath)) { // 创建流来读取文件并下载 const stream = fse.createReadStream(filePath) // 写入文件 stream.pipe(res) }对于前端的话,咋们需要使用一个库,就是 streamsaver,这个库调用了 TransformStream api来实现浏览器中把文件用流的方式保存在本地的。有了这个后,那就非常简单的使用啦😄😄😃const downloadFile = async () => { // StreamSaver // 下载的路径 const url = 'http://localhost:4001/download?filename=b0d9a1481fc2b815eb7dbf78f2146855.zip' // 创建一个文件写入流 const fileStream = streamSaver.createWriteStream('b0d9a1481fc2b815eb7dbf78f2146855.zip') // 发送请求下载 fetch(url).then(res => { const readableStream = res.body // more optimized if (window.WritableStream && readableStream?.pipeTo) { return readableStream.pipeTo(fileStream) .then(() => console.log('done writing')) const writer = fileStream.getWriter() const reader = res.body?.getReader() const pump: any = () => reader?.read() .then(res => res.done ? writer.close() : writer.write(res.value).then(pump)) pump()
unocss 是什么,不清楚的可以看这边 重新构想原子化 CSS,整体的架构1.vue3 + setup + ts, vw + rem等来搭建的移动端项目2.tslint, prettier来控制代码的格式3.simple-git-hook来控制代码提交的规范4.deploy.sh 来实现自动部署5.unocss 及其生态来实现css和icon图标的按需加载,不需要使用js就能引入图标6.记录滚动条位置,监听物理键返回,路由动画等都是hooks的形式存在项目效果图代码地址: https://github.com/cll123456/template-varlet-v3-ts演示环境: https://cll123456.github.io/template-varlet-v3-ts在断断续续的几天中,把项目实现了,做的效果还是让自己满意的。从开发体验来说是真的香,一直从js, css, icon图标等都遵从按需加载的原理。外围效果标配的移动端项目,移动端的适配也是做好了,vm + rem的形式来的。上中下的布局,黑暗模式等第一个页面进入时候的动画是渐变,进入详情是右边切入动画,离开详情是左边切入的动画,看起来还挺好看的。🎈🎈🎈第二个页面第二个特点是实现移动端物理键的控制,换句话说是这里实现了监听物理按钮的返回来做一点你想要的事情。第三个页面由于varlet提供了无限滚动的组件,如果无限滚动的数据太多,那么dom数量达到一定的量级就会卡顿,所以我在此基础上加上了虚拟dom的形式来节约性能。注意看,每一个列表的高度可是不固定的哦!第五个页面在移动端上面,经常会有需要返回到滚动条指定的位置,也就是说记录滚动条的位置,这里也是实现了哦🎈🎈彩蛋想要这一系列文章吗?那就请督促我更新吧!😄😄😄让我看到大家的热情,请评论转发告诉我,最后能不能给个小星星呢😁
引言2022年都过去两月了,是时候开始学起来了。从哪里开始呢?那就从未来的趋势 ssr来动手吧,现在vue3也出来这么久了,ssr怎么搭建呢?那咋就一起来康康吧🎉。项目源码:github.com/cll123456/v…项目演示地址:chenliangliang.top:9022/ (这个地址不能保证长期有效,但是上面的源码地址一般不会删除,有兴趣的可以直接clone源码跑起来);正文流程ssr的流程,有一张经典的图,如下:从上面的这张图咋们可以得出以下结论:图中包含source(资源),webpack, 服务端,这里是说资源通过webpack打包放到服务端;在资源这里咋们可以看到store, router,components 等都会通过咋们的app.js(main.js)来分为两个入口,一个是服务端入口,另一个是客户端入口来通过webpack进行打包;在服务端中先拿到给服务端打包的静态资源,然后通过一个render方法生成静态的html,此时咋们的页面结构就生成了,然后会发现有了结构里面所有vue的功能都用不了了,所以最后需要通过客户端来进行激活。最后就ok了 🎉🎉🎉依赖安装咋们来分析下,需要实现上面的功能,咋们需要安装哪些包?服务端: express (koa,egg,等搭建服务的都行)、nodemon(监听服务启动)客户端: vue、vue-router、vuex、sass、@vue/server-renderer(把服务端的bundle转成html)构建包的工具:webpack、webpack-cli、webpack-dev-server(可选)、webpack-merge(合并webpack的配置项)loader:babel-loader、@babel/core、@babel/preset-env、vue-loader``css-loader、vue-style-loader、sass-loaderplugins:@vue/compiler-sfc、html-webpack-plugin暂时咋们就先用这些基本的,把项目的结构先搭建起来,等一下遇到问题按需安装对应的依赖建立项目结构vue3-webpack-ssr ├─ entry │ ├─ app.js │ ├─ client.entry.js │ ├─ router.js │ ├─ server.entry.js │ └─ store.js ├─ package.json ├─ public │ ├─ favicon.ico │ └─ index.html ├─ server │ └─ index.js ├─ src │ ├─ App.vue │ ├─ components │ │ └─ Hello.vue │ ├─ Index.vue │ └─ Mine.vue └─ webpack ├─ base.config.js ├─ client.dev.config.js ├─ client.pro.config.js └─ server.config.js 复制代码咋们先把src里面的东西先写好吧,src里面的东西都是非常基本的,可以自己来随便写哦,详情查看gitup里面的内容: github.com/cll123456/v…开发入口文件入口分为客户端和服务端的两个入口在上图中咋们得知,两个入口都用到了app.js, 那么咋们就先来做这个.import { createSSRApp } from 'vue'; import App from './../src/App.vue'; // 对外导出一个函数,使用vue3的createSSRApp这个函数,详情请查看文档 https://v3.cn.vuejs.org/guide/ssr/hydration.html#%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%BF%80%E6%B4%BB-hydration export default function(){ return createSSRApp(App); 复制代码然后咋们先写客户端的,新建client.entry.js: import myCreateApp from './app'; const app = myCreateApp(); // 挂载节点 app.mount('#app') 复制代码最后是服务端的代码,server.entry.jsimport myCreateApp from './app'; import { renderToString } from '@vue/server-renderer' export default function (ctx) { return new Promise(async (resolve, reject) => { const app = myCreateApp(); // 把app变成html的代码给服务端调用 let html = await renderToString(app); resolve(html) 复制代码webpack的配置项既然有两个入口,那肯定是webpack需要打包多端,个人的喜好是分开来进行打包,这样更不会那么混乱,想要合并到一个文件夹的也行,就行区分环境即可。项目新建 webpack文件夹;在里面新增 base.config.js 这里面放的是服务端和客户端共有的配置,如下const path = require('path'); const { VueLoaderPlugin } = require('vue-loader'); module.exports = { // 输出 output: { path: path.resolve(__dirname, './../dist'), filename: '[name].bundle.js', // loader module: { rules: [ // 匹配 .vue文件用vue-loader { test: /\.vue$/, use: 'vue-loader' }, // 解析css,这个loader是从后往前执行,就是说 先执行 css-loader,然后在执行 vue-style-loader test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' // 解析sass test: /\.s[ac]ss$/i, use: [ // Creates `style` nodes from JS strings "vue-style-loader", // Translates CSS into CommonJS "css-loader", // Compiles Sass to CSS "sass-loader", // 对js使用loader来进行转换,配置对应的预设和排除一些不需要转换的文件 test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] exclude: /node_modules/ plugins: [ // 不管服务端,还是客户端都需要打包vue的结构 new VueLoaderPlugin(), 复制代码公共的配置写好后,咋们来写客户端的配置,建立文件 client.pro.config.js 内容如下:const { default: merge } = require('webpack-merge'); const base = require('./base.config.js'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // 合并默认的配置 module.exports = merge(base, { mode: "production", // devtool: 'source-map', entry: { 'client' : path.resolve(__dirname, '../entry/client.entry.js') output:{ // 清除元宵打包的结果 clean: true, // 客户端的文件名命 filename: '[name].client.bundle.js', plugins: [ // 使用html作为挂载的模板模板 new HtmlWebpackPlugin({ filename: 'index.html', template: path.resolve('public/index.html') 复制代码最后还差服务端,那咋们就来写,新建: server.congif.jsconst { default: merge } = require('webpack-merge'); const base = require('./base.config.js'); const path = require('path'); const nodeExternals = require("webpack-node-externals"); module.exports = merge(base, { // 模式是生产模式, mode: "production", entry: { 'server': path.resolve(__dirname, '../entry/server.entry.js') output: { filename: '[name].server.bundle.js', // node的代码环境是commonjs哦 library: { type: 'commonjs2' // 需要忽略css哦 externals: nodeExternals({ allowlist: [/\.css$/], // 打包的环境是node target: 'node', 复制代码开发服务端客户端好了,接下来就是服务端了,咋们启动一个服务,来调用我们对应的结果。在server目录中新建index.jsconst express = require('express') const server = express(); const path = require('path'); // 获取服务端打包的结果,一个获取html的函数 const createApp = require(path.join(__dirname, './../dist/server.server.bundle.js')).default; const fs = require('fs'); // 搭建静态资源目录 server.use( '/', express.static(path.join(__dirname, '../dist'), { index: false }) // 获取模板 const indexTemplate = fs.readFileSync( path.join(__dirname, './../dist/index.html'), 'utf-8' // 匹配所有的路径,搭建服务 server.get('*', async (req, res) => { try { const appContent = await createApp(req); const html = indexTemplate .toString() .replace('<div id="app">', `<div id="app">${appContent}`) res.setHeader('Content-Type', 'text/html'); res.send(html); } catch (error) { console.log(error); if (error.code == 404) { res.status(404).send('页面去火星了,找不到了,404啦'); return; res.status(500).send('服务器错误'); server.listen(9022, () => console.log('the server is running 9022')); 复制代码此时咋们就可以进行客户端打包和服务端打包,并且可以在服务端看到对应的效果了。看到这里有人就要说了,你页面怎么是带有颜色的,并且v-moel和事件也有了。我这里为了方便演示是进行了的。或者是说为了方便后面的同学在学习的时候有成就感,不会知道那么难,敢下手。😁😁😁激活流程1.我们仔细看配置,在打包服务端的时候咋们是不是会把客户端的js自动注入到dist/index.html中,并且在创建应用的时候咋们就告诉vue了(使用createSSRApp)2.在服务端只是构建了一个静态的html结构给服务端让服务端去拼接的同时,咋们也使用了dist/index.html作为模板来拼接其他的html.3.当咋们访问服务端的服务的时候,咋们还搭建了一个静态服务来提供其他资源的访问4.当这些步骤下来,客户端就会使用服务端的html的结构并且去激活它,拥有vue的特性。走到这里一个基本的ssr就完成了,接下来是加入vue-router了。加入vue-router加入路由的第一步是先随便加入些页面,使用路由来进行控制,然后咋们在来做下一步。在enter中加入 router.js,并且导出一个路由函数:import { createRouter } from 'vue-router' const routes = [ { path: '/', component: ()=> import('./../src/Index.vue') }, { path: '/mine', component: ()=> import('./../src/Mine.vue') }, // 传入不同的模式来进行配置 export default function (history) { return createRouter({ history, routes 复制代码在客户端加入路由的配置,在 client.entry.js 新增如下:import createRouter from './router.js' import { createWebHistory } from 'vue-router' const router = createRouter(createWebHistory()) app.use(router); // 原来的配置... // 在客户端和服务端我们都需要等待路由器先解析异步路由组件以合理地调用组件内的钩子。为此我们会使用 router.isReady 方法 router.isReady().then(() => { app.mount('#app') 复制代码客户端变了,咋们的服务端的入口也需要做出改变,在server.enter.js新增如下:import { createMemoryHistory } from 'vue-router' import createRouter from './router.js' // ...原来的promise内 const router = createRouter(createMemoryHistory()) app.use(router); await router.push(ctx.url); await router.isReady(); // 匹配路由是否存在 const matchedComponents = router.currentRoute.value.matched.flatMap(record => Object.values(record.components)) // 不存在路由 if (!matchedComponents.length) { return reject({ code: 404 }); // ... 其他的配置 复制代码注意✨✨✨! 这里客户端和服务端的是哟个的路由模式是不一样的,为啥呢? 因为hash模式的路由提交不到服务器上,并且服务端也可以有自己的路由,和客户端是不一样的哦接下来就是欢快的打包环节了。 结果就报错了……😂😂😂这个问题是老生常谈的babel的转换问题,就是我们使用了awync, await.那咋们给转转去加入依赖:@babel/plugin-transform-runtime 、 @babel/runtime-corejs3 然后修改webpack中服务端的配置,在server.config.js 新增如下:// ...原来的配置 module: { rules: [ test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [["@babel/plugin-transform-runtime", { "corejs": 3 exclude: /node_modules/ 复制代码然后vue-router就可以使用啦!加入vuex加入vuex还是一样的,建立store,修改客户端入口文件和服务端入口文件。在enter文件夹中加入store.js;import { createStore as _createStore } from 'vuex'; // 对外导出一个仓库 export default function createStore() { return _createStore({ state: { // 状态数据 msg: '' mutations: { // 同步数据 SET_MSG(state, mgs){ state.msg = mgs; actions: { // 异步数据 asyncSetMsg({commit}){ return new Promise((resolve) => { setTimeout(() => { commit('SET_MSG', '我是store中的msg'); resolve(); }, 300) modules: {} 复制代码修改客户端入口, 在 client.entry.js新增:const store = createStore(); // 判断window.__INITIAL_STATE__是否存在,存在的替换store的值 if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); app.use(store) 复制代码修改服务端入口,在server.entry.js新增: // 处理store Promise.all(matchedComponents.map(component => { if (component.asyncData) { return component.asyncData(store) })).then(async (res) => { let html = await renderToString(app); html += `<script>window.__INITIAL_STATE__ = ${replaceHtmlTag(JSON.stringify(store.state))}</script>` resolve(html); }).catch(() => { reject(html) 复制代码上面的asyncData是怎么做的呢?在setup外部直接定义哦!那咱们就可以欢快的打包,到最后时刻了!结果这里判断store有没有服务端渲染的条件是 刷新页面,store里面的值是一起出来的哦!
在上两篇文章中,mongo 进阶之——聚合管道, mongo 的安装与基本的认识, 中介绍了mongo的基本用法,但是我们在node环境中直接使用原生的,还是没有很方便。使用mongo不管是哪种代码的环境,如果需要连接数据库,都需要使用数据库的驱动。(以下代码的环境是node为例)。在node环境中,mongodb的驱动就叫做mongodb,但是这个驱动也有一个缺陷,模型的验证做的不是很好,一般我们都会使用mongoose 这个库来自定模型,验证参数等mongoose官网:mongoosejs.com/mongoose民间中文网:www.mongoosejs.net/mongoosemongoose 在node环境中类似于连接MySQL的se’sequelize` 都是用于定义模型,校验规则是否满足条件,然后在把模型映射到对应的数据库中。mongoose 与 mongodb的关系mongoose 自己有一套风格和apimongoose内部的还是使用mongodb的官方驱动,去操作mongo数据库的。mongoose 的schema: 结构,描述某种数据中有哪些字段、每个字段是什么类型、每个字段的约束等使用方式既然是一个驱动,那肯定是需要安装的,安装的方式也是很简单,如: npm install mongoose or yarn add mongoose等环境要求安装好驱动后,你需要保证你本地是有已经启动mongo服务的。检测方式如下: win + r —> 输入 services.msc这个服务需要启动着,才能使用mongoose 来连接,并且操作数据库。连接数据库var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost:27017/test1'); var db = mongoose.connection; db.on('error', function(){ console.log('连接失败,请查看mongo服务是否启动') db.on('open', function() { console.log('能够正常访问数据库') 复制代码如果你的服务没有启动,那么你会得到下面的结果:正常的结果是这样的:通过上面的代码,大家和我可能都会有一个问题,为啥mongoose中连接数据库,不需要用户名和密码呢?数据库我们知道是动态创建的,但是用户名和密码是怎么一回事呢?带着问题往下看:我带着这个问题去百度了下,加上自己对这方面的理解,得出了下面结论。 mongo 本身的数据库都是动态的,人家根本就不知道你连接的是哪台数据库,自然在权限方面肯定就是空的,怎么体现呢?如下:在robot 3t 上可以看到权限中的用户名和密码都是空的呢,只要你启动了服务,那么你就可以任意的创建数据库,任意的操作数据。这个是不是有点尴尬呀!所以在正式的开发中,一定切记需要做好数据权限问题,不然每个人都可以操作数据库,这是一件很危险的事情。
在工作的途中,很多时候会发现一些特别使用的组件。我们需要把我们有用的组件分享给大家,如果大家都这么做,何愁国家的科技实力不强。(也可能导致行业更加内卷)效果本次手把手记录的是一个vue3的给dom生成水印的指令,这里把他封装成一个组件,下一次直接拉下来就可以使用。这个可以用在移动端或者是pc端上防止数据被人截屏。效果如下:源码仓库:github.com/cll123456/v…准备工作这里我们可以思考下,写一个组件的步骤是啥?第一步:配置环境配置环境,在这个组件中,要做的是项目如何打包,使用的语言等?安装依赖在开发环境需要把代码进行打包,测试。这里使用rollup进行打包,如果对rollup有疑问的,可以查看往期的文章 "@types/jest": "^27.0.2", ### jest的类型检查库 "@vue/test-utils": "^2.0.0-rc.16", ### vue官方的测试工具 "eslint": "^8.1.0", ### 代码检查 "jest": "^27.3.1", ### jest 是一个令人愉快的 JavaScript 测试框架 "prettier": "^2.4.1", ### 代码格式的检查工具,和esline类似 "rollup": "^2.59.0", ### 代码进行打包的工具 "rollup-plugin-typescript2": "^0.30.0", ### rollup 转义typescript 的插件 "ts-jest": "^27.0.3", ### jest在测试ts代码的预处理库 "typescript": "^4.3.5", ### 强大的类型检查库 "vue": "^3.2.16", ### vue3 "@vue/vue3-jest": "^27.0.0-alpha.3" ### jest 测试vue的代码 "rollup-plugin-dts": "^4.0.0" ### rollup 自动生成类型文件 "rollup-plugin-terser": "^7.0.2" ### rollup 对代码进行压缩 复制代码安装完成上面的依赖后,咋们就需要开始进行配置文件了。新建tsconfig.json用于编写ts的规则{ "compilerOptions": { "target": "es6", // 编译的模板是es6, "moduleResolution": "node", // 模块解析策略是node "strict": true, // 启动严格模式 "importHelpers": true, // 开启模块导入助手 "esModuleInterop": true, // 开启模块的相互转换 "allowSyntheticDefaultImports": true, // 允许异步导入 "noImplicitThis": false, // 允许隐士的this "declaration": true, // 需要生成.d.ts的类型文件 "baseUrl": "./", // 根路径 "lib": ["esnext", "dom", "dom.iterable", "scripthost"], // 需要使用的资源库 "paths": { "@/*": ["src/*"] // 相对路径的指向 "include": ["src/**/*.ts", "tests/**/*.ts"], // 检查这里包含的文件 "exclude": ["node_modules","dist"] // 这里包含的文件不进行检查 复制代码有了类型检查,接下来配置一下rollup的默认配置这里只说明几个关键的点,在rollup中使用插件的顺序需要注意,不然容易报错,在本项目中, 插件的顺序应该是先进行 ts的转换, 然后进行代码压缩,最后是生成声明文件,具体的文件配置文件内容查看: github.com/cll123456/v…接下来就是eslint,prettier,jest的配置了,这两个配置也是比较简单的,一个是配置代码的检查规则,另一个是配置测试代码的规则。eslint配置请查看:github.com/cll123456/v…prettier的配置请查看: github.com/cll123456/v…jest的配置请查看: github.com/cll123456/v…如果看到这里的话。恭喜你,你的环境就好了,接下来请尽情的撸代码吧!第二步: 在环境中写代码思路分析需要在dom中生成水印,无非就是在dom中加入一张背景图片,然后在背景图片中加入想要的内容。/** * 添加水印 * @param str 水印的内容 * @param parentNode 父节点 * @param font 水印文字大小 * @param textColor 水印颜色 * @param rowLength 一个水印的宽度是多少 * @param colLength 一个水印的长度是多少 function addWaterMarker(str: string, parentNode: HTMLDivElement, font: string, textColor: string, rowLength: number, colLength: number) {// 水印文字,父元素,字体,文字颜色 let can = document.createElement('canvas'); parentNode.appendChild(can); can.width = parentNode.offsetWidth; can.height = parentNode.offsetHeight; can.style.display = 'none'; let cans = can.getContext('2d'); cans?.rotate(-10 * Math.PI / 180); cans!.font = font || "16px Microsoft JhengHei"; cans!.fillStyle = textColor || "rgba(180, 180, 180, 0.3)"; cans!.textAlign = 'left'; cans!.textBaseline = 'middle'; // 需要遍历添加文字 for (let row = 0; row < can.height / rowLength; row++) { for (let col = 0; col < can.width / colLength; col++) { cans?.fillText(str, col * colLength, row * rowLength); // 在节点中添加内容 parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")"; 复制代码上面是生成水印的核心代码,就看你放在哪里了测试代码代码写好后,测试也是很关键的、本项目使用了两种测试方法jest 测试: 这个测试过程也是很简单,只是简单测试,属性存在与否。建立项目的方式进行测试,这里涉及一个知识点,请查看测试的结果如下:第三步:分享到npm上当测试完成时候,就需要发布了。发布流程如下:1.使用命令npm login登录 2. 使用命令npm publish发布包
若依V3 #首先明确一个问题,这里不是官方的。我个人并不是若依团队的成员,只是一个开发爱好者。官方请查看项目文档地址:http://ruoyi-doc.chenliangliang.top/项目演示地址:http://ruoyi.chenliangliang.top/项目代码地址:https://github.com/cll123456/rouyi-cloud-vue3动机 #个人比较喜欢vue3的开发方式,目前也是全职前端,使用vue来作为技术站开发。在这里使用若依,并没有打算抄袭人家的成果,只是偶然在gitee的评价中看到,官方目前没有打算使用Vue3来更新前端。TIP既然人家现在没有时间,那就我先来试试水。看看会不会存在重大问题,顺便让自己对知识的理解更加深刻!!! 🎉 💯 ``项目结构 #本项目的后端是若依的后台,前台使用的是Vite, VUE3.3.20, Vue-router, Vux, element-plus, Scss, axios等。由于使用了vite作为构建工具,项目在启动和热加载都是非常快的哦!💯代码规范 #项目完全采用目前最新语法setup形式来写的: 代码主要改造的是js,其他的都高度还原了原来的成分,主要是为了大家方便使用。代码中的js也都基本写了注释,方便大家理解代码的意思。项目效果 #项目目前采用的ui和若依保持高度一致,增加了自定义主题,默认生成warning, info, 和success等
初衷写这个包的主要目的是为了使用vue-demi来写vue2和vue3的公用组件。简单说一下自己的开发感受吧。不没有想象中的那么顺利(可能是自己没有理解到位); 使用vue-demi 里面目前来说只能vue2和vue3选择一种来进行测试,如果你想在同一个项目中对vue2和vue3来切换测试,我没有做到,会有些问题。比如: 我曾在项目中建立了一个examplev2和examplev3来进行项目测试,vue3正常启动,vue2就会启动不了。我使用的是yarn workspace来进行搭建的。所以全局只能有一个vue,vue2我就重命名了,重命名后的结果就是vue-template-complier 里面不能识别我的vue2. 所以自己就只能单独搭建项目来进行测试希望有大佬可以做到在同一个项目中能够切换vue2和vue3的测试。目前我看到的线上的包,我fork下来看,人家的vue2也是有问题的。使用方式vue3npm i vue-login-slide-validatoryarn add vue-login-slide-validator案例<template> <div> <button @click="getStatus">获取状态</button> <button @click="reset">重置</button> </div> <div> <slide-validator :key="keys" width="300px" :slider-success-style="{backgroundColor: 'lightgreen'}" :success-bg-color="'#ccc'" ref="sliderRef"></slide-validator> </div> </template> <script setup> import slideValidator from "vue-login-slide-validator" import "vue-login-slide-validator/index.css" import { ref } from 'vue' const sliderRef = ref(null); const keys = ref(0); const getStatus = () => { console.log(sliderRef) alert(sliderRef.value.slideValidatorStatus) const reset = () => { keys.value = Date.now(); </script> <style> line-height: 50px; </style>效果vue2npm i vue-login-slide-validator @vue/composition-api或者yarn add vue-login-slide-validator @vue/composition-api案例<template> <div> <div> <button @click="getStatus">获取状态</button> <button @click="reset">重置</button> </div> <div> <slide-validator :key="keys" width="300px" :slider-success-style="{backgroundColor: 'lightgreen'}" :success-bg-color="'#ccc'" ref="sliderRef"></slide-validator> </div> </div> </template> <script> import SlideValidator from "vue-login-slide-validator" import "vue-login-slide-validator/index.css" import { ref } from "@vue/composition-api"; export default { name: 'App', components: { SlideValidator setup() { const sliderRef = ref(null); const keys = ref(0); const getStatus = () => { console.log(sliderRef) alert(sliderRef.value.slideValidatorStatus) const reset = () => { keys.value = Date.now(); return { getStatus, reset, keys, sliderRef </script> api中文意思属性名称默认值类型一开始背景颜色backgroundColor#abcdefstring成功的背景颜色successBgColor无string宽度width300pxstring高度height50pxstring初始内部文字innerText向右拖动滑块验证string成功后的滑块文字sliderSuccessInnerText验证成功string槽内样式innerTextStyle无StyleValue滑块一开始的样式sliderStyle无StyleValue滑块成功的样式sliderSuccessStyle无StyleValue获取滑块状态slideValidatorStatusfalseboolean源代码请查看 https://github.com/cll123456/test-demi.git
esbuild 相信在使用过vite的同学都知道,vite是开发环境使用的是esbuild来进行编译代码的,生成环境打包使用的是rollup,想看rollup的同学,可以查看我的往期文章。(实战 rollup 第一节入门) (rollup 实战第二节 搭建开发环境)(rollup 实战第二节 搭建开发环境)使用方式esbuild的官网里面有说明,esbuild的使用方式,提供三种方式: cli, js, go搭建开发环境既然需要使用esbuilld, 那肯定是需要安装esbuild的依赖的, npm install esbuild -D的。这里需要说明一下,esbuild, 他提供的不是类似webpack,rollup的配置文件,而是提供一些转换函数(在node环境或者go环境来执行的),直接来帮你转换代码的。本人是一名前端开发工程师,对于go不了解,这里也只做js的两种方式来配置开发环境。方式一 cli使用cli的方式这个和webpck,rollup一样,给我们做出了许多的指令: --bundle --w --outfile等.在 package.json中的script中添加以下一个脚本: "build": "esbuild src/index.js --bundle --outdir=dist/index.js"这句话的意思是说,使用esbuild 加载src/index.js 为入口,打包并且以dist/index.js为出口。效果方式二 使用api在src中新建esbuild.config.js,这个js里面不是配置文件,而是使用api来调用的方式.const esbuildConig = () => require('esbuild').buildSync({ entryPoints: ['src/index.js'], bundle: true, outfile: 'out.js' esbuildConig(); 复制代码在package.json中的script新建脚本如下:"buildConfig": "node ./esbuild.config.js" # 这里是node来执行那个js,然后通过api来进行打包,转译 复制代码编译结果启动开发服务esbuil本身提供对外启动一个服务,但是我想直接编译,然后手动来启动一个静态资源的服务,实现代码改变后自动刷新浏览器,主要的文件是 ts 和 css文件。效果如下服务启动后:启动服务后可以访问我们的项目,然后当我们修改代码后面。自动更新浏览器。服务暂停后:这个在服务断开后是不是很熟悉。思路esbuild 的工作内容很简单,快速打包资源。对资源进行编译,有点babel的意思。用在开发阶段相当舒服。调试代码很轻松。第一步 : 打包开发环境代码esbuild 提供了几个关键转译函数,我们就使用build就好了,为了方便我们看到内容,所以把开发环境的代码也生成到本地(一般是不生成代码的,打包后代码从本地读写要花费时间,vite,webpack等都是把打包的结果放内存中的)第二步 : 启动一个服务esbuild 本身会对外可以提供服务,但是文档里面写的并不是很清楚,况且 vite 开发环境也是自己使用koa来提供服务的,所以咋们也来自己使用koa提供服务。并且方便生成 index.html文件绑定对应的 js 和 css等.第二步 :监听文件变化,并且告知浏览器更新这里要区分一个点,那就是vite, webpack 等实现的热更新是hmr(hot module reload),hrm 是保留当前状态,自动页面局部更新,不需要刷新整个页面,具体的思路查看。 而我们这里就不做那么复杂,只要修改代码我就给你刷新浏览器。体验esbuild的开发过程嘛.核心代码如下// esbuild.config.js文件 const esbuild = require('esbuild'); const serve = require('koa-static'); const Koa = require('koa'); const path = require('path'); const fse = require('fs-extra') const app = new Koa(); // 启动编译好后自动刷新浏览器 const livereload = require('livereload'); const lrserver = livereload.createServer(); lrserver.watch(__dirname + "/dist"); // 使用静态服务 app.use(serve((path.join(__dirname + '/dist')))); esbuild.build({ // 入口 entryPoints: ['src/index.ts'], // 启动sourcemap sourcemap: true, // 打包 bundle: true, // 输出的目录 outfile: 'dist/index.js', // 启动轮询的监听模式 watch: { onRebuild(error, result) { if (error) console.error('watch build failed:', error) else { // 这里来自动打开浏览器并且更新浏览器 console.log('\x1B[36m%s\x1B[39m', 'watch build succeeded') }).then(async res => { const fileName = path.join(__dirname + '/dist/index.html'); // 创建文件,如果文件不存在直接创建,存在不做任何事情 await fse.ensureFile(fileName); // 把下面内容写入dist中的index.html文件中 await fse.writeFileSync(fileName, ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>测试esbuild</title> <link rel="stylesheet" href="./index.css"> </head> <body> <h1>测试esbuild</h1> <div id="app"> APP </div> </body> <script type="module" src="./index.js"></script> <script> document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>') </script> </script> </html> `) // 启动一个koa静态资源服务 app.listen(3000, () => { console.log(`> Local: http://localhost:3000/`) 复制代码这里就只放一下关键代码和思路,具体查看:https://github.com/cll123456/myPackage/tree/esbuild-dev其他指令Simple options: # 基础配置 --bundle Bundle all dependencies into the output files #打包所有的依赖进输出文件 --define:K=V Substitute K with V while parsing # 在解析代码的时候用V替换K --external:M Exclude module M from the bundle (can use * wildcards) # 从模块中排除M(可以使用*作为通配符) --format=... Output format (iife | cjs | esm, no default when not bundling, otherwise default is iife when platform is browser and cjs when platform is node) # 输出的文件格式(iife | cjs | esm,不打包不默认,浏览器默认iife,node环境默认cjs) --loader:X=L Use loader L to load file extension X, where L is one of: js | jsx | ts | tsx | json | text | base64 | file | dataurl | binary #当L是以下文件中的一个(js | jsx | ts | tsx | json | text | base64 |file | dataurl | binary)使用loader L来进行拓展X --minify Minify the output (sets all --minify-* flags) # 代码压缩(设置所有使用 --minify-*) --outdir=... The output directory (for multiple entry points) # 文件输出目录(对于多个入口点) --outfile=... The output file (for one entry point) #文件输出名称(对于单个入口点) --platform=... Platform target (browser | node | neutral, default browser) # 编译的环境 (browser | node | neutral, 默认浏览器) --serve=... Start a local HTTP server on this host:port for outputs # 在host:port的基础上开启一个http服务来输出文件 --sourcemap Emit a source map # 输出source map文件 --splitting Enable code splitting (currently only for esm) # 代码分割(当前仅限 esm模式) --target=... Environment target (e.g. es2017, chrome58, firefox57, safari11, edge16, node10, default esnext) # 代码打包结果的环境(e.g. es2017, chrome58, firefox57, safari11, edge16, node10, 默认esnext语法) --watch Watch mode: rebuild on file system changes # 监听模式: 改变文件后重写编译 Advanced options: # 高级配置 --allow-overwrite Allow output files to overwrite input files # 是否允许输出的文件覆盖源文件 --asset-names=... Path template to use for "file" loader files (default "[name]-[hash]") # 静态资源输出的文件名称(默认是名字加上hash) --banner:T=... Text to be prepended to each output file of type T where T is one of: css | js # 在输出的 js, css文件中添加一段文本 --charset=utf8 Do not escape UTF-8 code points # 代码字符集,不要做其他的转换,默认(utf-8) --chunk-names=... Path template to use for code splitting chunks (default "[name]-[hash]") # 分割chunks的名称(默认名字+hash) --color=... Force use of color terminal escapes (true | false) # 终端输出是否带颜色 --entry-names=... Path template to use for entry point output paths (default "[dir]/[name]", can also use "[hash]") # 入口点输出路径的路径模板(默认 dir/hash, 也可以是hash) --footer:T=... Text to be appended to each output file of type T where T is one of: css | js # # 在输出的 js, css文件中结尾添加一段文本 --global-name=... The name of the global for the IIFE format # 输出文件类型是 iife的全局名称 --inject:F Import the file F into all input files and automatically replace matching globals with imports # 将文件F导入所有输入文件,并用导入自动替换匹配的全局变量 --jsx-factory=... What to use for JSX instead of React.createElement # JSX使用什么代替React.createElement --jsx-fragment=... What to use for JSX instead of React.Fragment # jsx 使用什么代替 React.Fragment --jsx=... Set to "preserve" to disable transforming JSX to JS #设置为“preserve”以禁用将JSX转换为JS --keep-names Preserve "name" on functions and classes # 保留函数和类的名称 --legal-comments=... Where to place license comments (none | inline | eof | linked | external, default eof when bundling and inline otherwise) # 注释采用怎么的形式保留 --log-level=... Disable logging (verbose | debug | info | warning | error | silent, default info) # 控制台log输出的形式 --log-limit=... Maximum message count or 0 to disable (default 10) #最大的消息数量 --main-fields=... Override the main file order in package.json (default "browser,module,main" when platform is browser and "main,module" when platform is node) # 覆盖package.json中的字段,根据不同的平台有不一样的覆盖方式 --metafile=... Write metadata about the build to a JSON file # 将元数据写入编译好的json文件中 --minify-whitespace Remove whitespace in output files #去除输出文件的空格 --minify-identifiers Shorten identifiers in output files #缩短输出文件中的标识符 --minify-syntax Use equivalent but shorter syntax in output files #在输出文件中使用等效但较短的语法 --out-extension:.js=.mjs Use a custom output extension instead of ".js" #使用自定义的后缀名来代替输出的js后缀 --outbase=... The base path used to determine entry point output paths (for multiple entry points) # 输出文件的根路径(对于多入口点) --preserve-symlinks Disable symlink resolution for module lookup #禁用模块查找的符号链接解析 --public-path=... Set the base URL for the "file" loader # 设置加载loader的跟路径 --pure:N Mark the name N as a pure function for tree shaking # 将标记名字为N的纯函数用于tree shaking --resolve-extensions=... A comma-separated list of implicit extensions (default ".tsx,.ts,.jsx,.js,.css,.json") # 以逗号分隔的隐式扩展列表 --servedir=... What to serve in addition to generated output files # 服务额外生成文件的输出目录 --source-root=... Sets the "sourceRoot" field in generated source maps # 在生成的源映射中设置“sourceRoot”字段 --sourcefile=... Set the source file for the source map (for stdin) #设置源映射的源文件(对于stdin) --sourcemap=external Do not link to the source map with a comment # 注释不需要链接到 source map --sourcemap=inline Emit the source map with an inline data URL #使用内联数据URL生成源映射 --sources-content=false Omit "sourcesContent" in generated source maps # 在生成的源映射中省略“sourcesContent --tree-shaking=... Set to "ignore-annotations" to work with packages that have incorrect tree-shaking annotations #设置为“忽略注释”以处理具有不正确的tree-shaking注释的包 --tsconfig=... Use this tsconfig.json file instead of other ones # 使用此tsconfig.json文件而不是其他文件 --version Print the current version (0.12.16) and exit #打印当前的版本并且退出 复制代码学习一个知识点,需要输入和输出,这样自己来能掌握的更多,看文档是一个输入的过程,但是写博客是一个输出的过程,希望能够更多的人使用这种方法来学习,科技强国 ,加油!!!
前面讲过了,rollup如何打包开发环境。现在肯定是打包成生产环境了。本次需要实现的功能是把库打包成生产环境。由于本次代码是基于前两次的基础上的,如果有问题的还请移步前两节。 (实战 rollup 第一节入门) (rollup 实战第二节 搭建开发环境)打包生umd, cjs, esm的文件vue 源码打包的分为以下几种(umd, cjs, esm)react 源码打包也是上面的三种(umd, cjs, esm)所有的学习都是从模仿开始,那咋们也来配置一下,打包成这三种类型的js.打包简单代码既然要使用rollup, 那肯定是需要安装rollup这个打包工具的,如果已经全局安装的请忽略,个人是局部安装的, npm install rollup -D, 然后在package.json中的script中加入 打包的命令 "build": "rollup -c ./build/rollup.config.js",然后在根目录下面建立 build文件夹配置文件这里的配置文件其实很简单,既然rollup的output属性配置的是一个对象,那么我配置成一个数组,里面包含多个输出对象,那不就有了。在build目录下面,建立一个名称叫做 rollup.build.js的文件,然后放入以下代码:export default { input: 'src/index.js', // 打包的内容,自己随便写一些简单的代码 output: [ file: 'dist/cjs/index.js', // 打包成commonjs format: 'cjs' file: 'dist/esm/index.js', // 打包成esmodule format: 'esm' file: 'dist/index.js', format: 'umd', name: 'index' // umd 规范,一定要有一个名字,不然打包报错 复制代码效果执行 npm run build ,然后在src同层目录下面就会生成一个dist文件夹,内容如下:测试效果对于如何测试本地的包,我在上篇文章说明了,请移步 npm 如何测试自己本地的包,这里就看效果就行,这个代码也是上一次测试本地链接里面的代码。打包现代代码现代代码就是在原有的基础上,加了es6以后的 promise, async, await, 生成器,类等,这些代码需要进行代码转译的。转译es的代码,大家都知道需要使用babel。但是rollup在转译代码方面官方做了一个插进,叫做 @rollup/plugin-babel,转译现代化代码。需要安装一下包。 npm install @rollup/plugin-babel @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/runtime-corejs3 @rollup/plugin-babel -D, 对于这些包的具体使用以及作用,请查看接下来在rollup.config.js 加上插件属性,配置文件添加以下代码:import babel from '@rollup/plugin-babel'; // 导入babel // 使用插件 plugins: [ babel({ exclude: 'node_modules/**', // 防止打包node_modules下的文件 babelHelpers: 'runtime', // 使plugin-transform-runtime生效 // 使用预设 presets: [['@babel/preset-env', { "modules": false, "useBuiltIns": "usage", // 目标浏览器 "targets": { "edge": '17', "firefox": '60', "chrome": '67', "safari": '11.1', 'ie': '10', plugins: [ // 多次导入的文件,只导入一次 ['@babel/plugin-transform-runtime']], 复制代码带着愉快的心情来打包代码,npm run build 结果如下:这一张报错信息告诉我们两个问题,QUS1 缺少依赖 core-js人家都告诉咋们怎么弄了,直接安装包就好。 npm install --save core-js@3然后在 "useBuiltIns": "usage",下面加入以下配置(由于本人安装的版本是3.15.2,所以我的值就是3.15.2,各位同学请结合实际情况来配置) "corejs": "3.15.2", 复制代码QUS2 有一堆的转译包找不到看到这个问题,最好的方法就是去看那些打包好的文件。我打包好的文件,那些转译的包都没有引入进来,怎么使用也是报错。解决办法安装以下两个包: npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs@rollup/plugin-node-resolve: 让rollup可以找到node环境的其他依赖@rollup/plugin-commonjs : 将commonJS代码转译成 esmodule的代码然后修改配置文件:import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; plugins:[ nodeResolve(), commonjs(), 复制代码那咋们继续来打包。在这里肯定有的人要说话了,为啥一点点代码,打包后却增加了这么多代码,因为本人在代码里面 使用了 类,promise, async, await, 并且还要兼容ie10, 作为一个包,肯定是能满足越多的人越好,一般的包都会支持这些的。测试这里的测试,咋们直接建立html文件,然后在外部使用 dev环境来测试ie10中查看效果恭喜你,同学,也恭喜我自己,到了这一步。一些基本的库的打包方式就已经好了。如何发布到npm, 请查看其他的博客。我这还没有完呢加入ts许多包都是用ts来写的,有关ts的相关请查看我的专栏,这里不做过多的解释。要使用ts来开发代码,咋们必须要安装ts才行,所以安装以下库:npm install typescript rollup-plugin-typescript2 tslibtypescript: ts的代码库rollup-plugin-typescript2 : 这个库比官方的那个@rollup/plugin-typescript的下载量多好几倍,肯定选它tslib:rollup-plugin-typescript2一起使用的库。 这是TypeScript的运行时库,包含所有TypeScript帮助函数 。类似 babel-core 与 babel的关系。修改配置文件 配置文件做以下修改:import typescript from 'rollup-plugin-typescript2'; plugins:[ typescript({ // 使用tsconfig.json的配置文件 tsconfig: "tsconfig.json", // 默认声明文件放到一个文件夹中 useTsconfigDeclarationDir: true 复制代码配置tsconfig.json在 src同级目录下面添加tsconfig.json,内容根据自己实际的情况添加,我这里给一个案例 ,如下:{ "compilerOptions": { // ts编译的目标文件,我这里编译成es6, 然后交给babel "target": "ES2017", // 使用最新的语法 "module": "esnext", // 库使用dom, dom可迭代 "lib": ["ESNext", "DOM", "DOM.Iterable"], // 模块解析策略是node "moduleResolution": "node", // 开启es与其他模块的交互 "esModuleInterop": true, // 开启严格模式 "strict": true, // 开启声明文件的输出 "declaration": true, // 允许导入json模块 "resolveJsonModule": true, // 跳过库的检查 "skipLibCheck": true, "noUnusedLocals": true, // 开启声明文件输出目录 "declarationDir": "./dist/types/" // 只编译src目录下面的文件 "include": [ "./src" 复制代码配置完后 ,我们来打包。npm run build:通过这张图,我们发现一个问题,代码比变少了,是es6的代码。这个代码肯定是不能在ie上使用的。那就是babel没有生效了?解决办法我们的ts转移后又没有输出,babel默认是不会对.ts文件后缀名进行转译的,所以我们需要加一个添加转译名。{ puligins:[ babel({ // 解析 拓展名为ts的文件 extensions: [ '.ts' 复制代码这样打包就行了。是不是很简单,但是你看到的简单,往往在写代码的时候需要写好几遍。测试的效果就不看了,和上面是一样的。压缩代码我们会发现,目前打出的包是没有对代码进行压缩的,上面的方式代码的可读性还是比较强的,所以接下来对代码就行压缩npm install rollup-plugin-terser -D 然后在最好使用这个插件就行。效果详细代码,请查看github.com/cll123456/m…
axios 大家都非常的清楚,一个既可以用于客户端或者 服务端发送http请求的库。但是在前后端联调的时候有的时候会很难受,所以这里我来做一个总结。希望能帮助到有缘人。参数的传递方式参数传递一般有两种,一种是 使用 params, 另一种是 data的方式,有很多的时候我们看到的前端代码是这样的。get 请求axios({ method: 'GET', url: 'xxxxx', params: param, axios({ method: 'GET', url: '/xxx?message=' + msg, })post 请求axios({ method: 'POST', url: '/xxxxx', data: param, axios({ method: 'POST', url: '/xxxxx', params: param, })正确传递传递参数的解决办法分为post和get,咋们从这里来看一下postpost 是大多数人会搞错的,咋们来看看。data 的形式从例子中说话,使用的案例代码是post参数,并且没有做任何的转码。 method: 'POST', url: '/xxxxx', data: param, })控制台结果使用data传递的是一个对象,在控制台中看到的话是 request payload参数的view sources如下:node 后台接收参数的方式这里我采用的是koa 来搭建的后台。需要使用 koa-bodyparser 这个插件来解析body 的参数import Koa from 'koa'; import bodyParser from 'koa-bodyparser' const app = new Koa(); // 解析body app.use(bodyParser()); app.listen(9020, () => { console.log('the server is listen 9020 port'); })接受方式如下:java 后台接收参数的方式对于 java 来说,本人并不是那么熟悉,但是知道的是。如果需要接受axios 以data 传递的参数。需要使用注解 @responseBody 并且使用的是实体类来接收的.post data 的形式 ,不管是 哪种服务端的语言,都需要从body中获取参数。主要用于 传递 对象的参数,后台拿到的数据是一个 obj。 data 形式的数据有可以做好多事情, 文件上传,表单提交 等params 的形式这个是一个对象形式传递的,案例代码如下: axios({ method: 'POST', url: '/xxxxx', params: param, })浏览器结果分析查看view sourcer 如下:node 后台接收参数的方式启动服务和上面一样,但是接收参数的方式有点变化java 后台接收参数的方式这个本人搞不来 ,理论上是从地址栏上获取参数。应该也是 使用某个注解吧get 请求get 请求不管使用哪种方式,最后的参数都会放到路径上。使用param 只是axios帮你把这个参数进行了序列化,并且拼接在 url上面。原因的话,请查看下面出现两种的原因遇到这个问题,咋们就需要去看 axios 的源码了.这里 只会看处理参数的部分。有兴趣的自己去查看源码。处理data在axios文件中 的 core/dispatchRequest.js 中,我们可以看到 ,axois会 data在 axios 的 default.js 中,有一个函数专门转换 data 参数的 。注意: 上面只是举例 data 传递参数的一种情况哈!其实data 也有在地址栏 上 拼接的情况,或者 是文件上传的等情况。太多了,这里 只是讲清楚使用的方式。处理 params在axios文件中 的 adapt/ xhr.js 中,我们可以看到 ,axois会 params的参数放到url路径中。buildUrl 一些关键代码如下 :总结其实前端和后端 对接参数过程,对于post请求,data 不行,那就使用 params来 进行 传递,如果都不行,那就可能后端有问题了。彩蛋后台测试数据可以使用 postman, 如果可以传递的参数可以调用通过。postman是可以看到请求的代码的方式的。
引言在上一篇博客中,我简单的描述了 rollup 怎么使用,配置文件的使用。这一篇,来一起学习一下 rollup 怎么搭建开发服务,这里不包含任何的框架代码,我们需要 实现的是 ,我 在代码中修改任何地方,rollup可以自己监听到,并且给我给我更新浏览器就行。 这里的代码包括 css, 以及js等。效果本需要实现的效果是,启动一个 rollup 开发服务,然后在界面上带有样式,控制台输出js的结果,当我们改变代码后(js,css)后,按 ctrl + s 保存代码后,自动热更新代码。目录说明my-rollup //项目名称 ├─ build // 打包文件夹 │ └─ rollup.config.dev.js // 开发环境配置文件 ├─ dist // 开发环境编译的结果目录 │ ├─ index.css // css的编译结果 │ ├─ index.html //入口文件 │ ├─ index.js // 对应的js │ └─ index.js.map // js map 方便在浏览器中定位代码错误的位置 ├─ package-lock.json // 安装包的依赖树文件 ├─ package.json // 安装的包 └─ src // 源码目录 ├─ add.js // 对外导出一个添加的方法 ├─ util.js // 工具js,导出一个延时函数,一个promise ├─ index.css // 样式文件 ├─ index.js // 入口的js文件 └─ reduce.js //对外导出一个减少的方法依赖安装说明这里对每一个依赖 进行简单的貌似和一些使用方式@rollup/plugin-babelrollup的模块机制是ES6 模板,并不会对es6其他的语法进行编译。因此如果要使用es6的语法进行开发,还需要使用babel来帮助我们将代码编译成es5。对于这种需求,rollup提供了解决方案rollup-plugin-babel,该插件将rollup和babel进行了完美结合。 Babel是一个 JavaScript 编译器,准确说是一个source-to-source编译器,通常称为“ transpiler”。这意味着您向 Babel 提供一些 JavaScript代码,Babel 修改代码,并返回生成新代码。会把源代码分为两部分来处理:语法syntax、api。语法syntax比如const、let、模版字符串、扩展运算符等。 api比如Array.findIndex(), promise等es6以后的新函数。使用方式import {babel} from '@rollup/plugin-babel' module.exports = { // 入口 input: 'xxxx', // 使用插件 plugins: [babel({ // 不转译,node_modules里面的代码 exclude: 'node_modules/**', }@babel/corebabel/core 是babel的核心包,由于 @rollup/plugin-babel 这个库里面需要引用这个库,所以为我们需要进行安装。这里简单描述一下这个库的作用。首先这个库是一个代码的编译器,将代码进行转译。里面包含了三个阶段。解析代码成语法树 : @babel/parser 是 @babel/parser用于将代码进行语法分析,词法分析,然后生成一个语法树 ( AST)对语法树进行代码替换:@babel/traverse 是 @babel/parser用于将一个AST,并对其遍历,根据preset、plugin进行逻辑处理,进行替换、删除、添加节点等操作,生成转译的代码 :@babel/generator 是 @babel/parser用于把上一步操作好的AST生成代码。babel转码流程:input string -> @babel/parser parser -> AST -> transformer[s] -> AST -> @babel/generator -> output string。使用方式@rollup/plugin-babel 这个 插件帮我实现了代码转译,我们不需要做额外的操作,只需要安装库就好了。不需要做任何的配置哦!@babel/preset-env现如今不同的浏览器和平台chrome, opera, edge, firefox, safari, ie, ios, android, 等不同的模块"amd", "umd" , "systemjs" ,"commonjs",esm等这些es运行环境对es6,es7,es8支持不一,有的支持好,有的支持差,为了充分发挥新版es的特性,我们需要在特定的平台上执行特定的转码规则,说白了就像是按需转码的意思使用方式使用方式大致有三种,第一种 ,建立 .babelrc文件, 第二种,在package.json中使用,第三种,在babel中进行配置。这里只介绍在babel配置中使用。import {babel} from '@rollup/plugin-babel' module.exports = { // 入口 input: 'xxxx', // 使用插件 plugins: [babel({ exclude: 'node_modules/**', //使用预设,按照我的目标来编译代码 presets: [['@babel/preset-env', { "targets": { "edge": '17', "firefox": '60', "chrome": '67', "safari": '11.1' }@babel/plugin-transform-runtime对代码进行转译,然后不会重复引用导入相同的代码,一般和 @babel/runtime-corejs3 一起使用使用方式{ "plugins": ["@babel/plugin-transform-runtime"] }rollup打包构建工具,主要用于构建代码库 。使用方式查看上篇博客 https://blog.csdn.net/qq_41499782/article/details/118725309rollup-plugin-serve搭建rollup 开发服务器,类似 webpack-dev-serve,功能也差不多使用方式 // 启动开发服务器 serve({ port: 5000, host: 'localhost', // 当遇到错误后重定向到哪个文件 historyApiFallback: resolveFile('dist/index.html'), // 静态资源 contentBase: [resolveFile('dist')], // 在开发服务中添加一些输出的一些信息 onListening: function (server) { console.log('\x1B[33m%s\x1b[0m:', 'The rollup dev Serve is start!!!') const address = server.address() const host = address.address === '::' ? 'localhost' : address.address // by using a bound function, we can access options as `this` const protocol = this.https ? 'https' : 'http'; console.log('\x1B[36m%s\x1B[0m', `Serve is listening in ${address.port}`); console.log('\x1B[35m%s\x1B[39m', `You can click ${protocol}://${host}:${address.port}/ go to Browser`); console.log('\x1B[34m%s\x1B[39m', `You can click ${protocol}://localhost:${address.port}/ go to Browser`); }),@rollup/plugin-html生成一个静态的html模板,这个和weback-plugin-min--html很像,是动态生成html的,会默认给你生成一个模板使用方式 html({ fileName: 'index.html', title: '测试rollup开发环境', }) rollup-plugin-livereload启动热更新,这个热更新是自动刷新浏览器的哦,更改css或者js都会自动的刷新浏览器使用方式// 开启热更新 livereload(),rollup-plugin-css-only这个是用于打包css的库 ,使用方式 css({ output: 'index.css' }),上面的这些库有的是插件,注意插件的运行顺序,从上到下,需要先使用 css,然后来解析 jss,这页面才能 更快出来哦! "@rollup/plugin-babel": "^5.3.0", "@babel/core": "^7.14.6", "@babel/preset-env": "^7.14.7", "@babel/plugin-transform-runtime": "^7.14.5", "rollup": "^2.53.1", "rollup-plugin-serve": "^1.1.0", "@rollup/plugin-html": "^0.2.3", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-css-only": "^3.1.0", package.json 脚本package.json 脚本变化的是,使用开发环境的配置文件,并且开启监听文件修改。"scripts": { "dev": "rollup -w -m -c ./build/rollup.config.dev.js"src 目录的内容index.js这里为了做测试,加入了promise 和 class, async 和 awaitimport { add, AsyncClass } from "./add"; import { reduce } from "./reduce"; import './index.css'; const arr1 = ['a', 'b', 'c']; const arr2 = [4, 5, 6]; const result = [...arr1, ...arr2]; console.log(result); const a = 1, b = 2; const res = add(a, b); const d = reduce(a, b); console.log(res); new AsyncClass().asyncAdd(10, 20).then(res => { console.log(res, '异步加结果') })详细代码,请查看GitHub仓库 https://github.com/cll123456/myPackage rollup-two 分支
最近node 更新了,我从 node14.6 升级到 node 16.4, 对于我新的项目来说,木有任何问题。但是今天要求在老项目中进行添加需求,发现代码运行竟然报错。项目启动报错第一次启动项目报错,报错内容是 Node Sass does not yet support your current environment: Windows 64-bit, 这个问题相信大家都清楚, node-sass 出问题了按照以往的经验,删除 node_modules, 然后重新按照 node-sass, 你会发现都报错,并且npm 安装也是一直报错如下:… 一系列莫名其妙的错误。然后我发现一个问题,node-sass 一直默认给我安装的版本是 4.1.4 ,去查看 npm 发现人家有好多版本,然后去看得出下面内容。解决查看git 上的 issues 发现了版本对应问题。 issues 地址, 版本对应地址到了这里,大家都可以知道,接下来要做的就是第一种方法: 卸载包,从新安装新的包。第二种方法: 降低node的环境我进一步多么不容易,怎么可能降级呢? 历史的潮流肯定是进步的,我也不能后退。头铁一下,把包升级。卸载卸载 node-sass sass-loadder sass 等相关 sass的包然后安装的话,需要安装 node-sass 6 的版本,默认给我们安装4,那就强制安装:npm install node-sass@6 sass-loadder sass -D 或者 npm install --save-dev node-sass@6 sass-loadder sass安装好包后,很愉快的接着报错:这个错误既然是sass-loader里面出来的,那我们就去issues 里面看看有没有人提过这样的问题。很幸运的是,有人提出了,并且给了解决办法如下: 原文地址搞了半天,原来要webpack5才支持 sass-loader 11(本人安装的是11), 那我肯定接着升级了,不服就干。webpack4 升级 webpack5我们先缕一缕, webpack4 相关的有啥, webpack, webpack-cli webpack-dev-serve 还有对应项目的webpack的插件。 那好,我都给卸载一下,然后重新安装。使用命令 :npm uninstall webpack webpack-cli webpack-dev-serve, 然后 npm ininstall webpack webpack-cli webpack-dev-servewebpack 升级带来的问题这个升级的配置文件和命令肯定有一些细枝末节的变化,详情查看官网命令修改配置文件的变化这些变化肯定是存在的,但是webpack5 做的启动速度还是挺好的。启动结果看到这里,肯定是成功了,不然也没有结果了,遇到问题不能怂,大不了我就git revese, 好好享受这个过程。开发环境结果生成环境结果引用https://github.com/webpack-contrib/sass-loader/issues/974https://github.com/sass/node-sass/issues/3106https://webpack.docschina.org/configuration/#options
这些天个人博客网站终于写好了,使用的技术是react17 + vite + redux + saga + ts等,后台使用的是 node + koa + mysql + ts ,前台地址是: http://blogs.chenliangliang.top/前端代码地址: https://github.com/cll123456/blog服务端代码地址: https://github.com/cll123456/my-blog-serve问题描述项目是写好了,但是我的首页加载出来竟然要20多s(服务器是最低标准,http协议)效果我使用另一个浏览器给大家截图,为了避免大家说是缓存啥的。看到效果后,大家都会觉得比较满意的,但是怎么做的呢?大家可能都知道,不就是 启动了个 gzip嘛。 对的,是这样的。思路这一步其实是最难的,对于一个只知道概念,但是不知道原理的人来说。所以一切从原理出发。zip 文件由哪端生成?这个是一个问题,网上大部分教程会告诉你,在服务端配置nginx, 然后 xxx 一波操作猛如虎。 但是对于新手来说,这样真的好吗?不告诉人家原理,是不行的。所以咋就是那个打破沙锅问到底的,不弄明白。觉也睡不好。服务端生成zip 文件可以服务端生成,例如:nginxnginx 有一个模块是 gzip 模块,然后你只要开启了,nginx就会帮你来把数据(静态资源 和 接口数据)进行压缩,然后传入到客户端,客户端来解压,然后在进行代码的读取,其实这一步就是节约带宽,减少传输的代码包的数量。从而节约传输时间。然后网站就能很快打开了。nodenode也有相关于 compression 的库,然后配置一些选项,来选择对数据(资源和接口数据)的压缩,这个是同一个道理,就是服务端来进行压缩嘛,然后在传输。其他的服务也有相关的库,怎么使用要看对于的语言了,这里就不展开客户端生成既然 服务端可以生成gzip文件, 那些构建工具 webpack, rollup, 等为啥也要写一些压缩的包? 而且会发现包好像周下载量还停高的。例如:为啥要客户端生成呢? 问得好, 我们知道服务端生成是不是每一次请求都要去请求服务器,然后服务器来生成压缩包。服务器每一次生成压缩包是不是会不会浪费服务端的性能哇!, 如果客户端生成,服务端先判断是否存在的后缀名为zip的文件,直接去拿,不存在在来压缩,这样是不是把服务器每一次都要压缩的事情,交给客户端了呢? 虽然客户端打包进行代码压缩会很慢。 但是我们打包只是发布代码的时候打一次包,而服务器是要面对成千上万的人来访问等。 说到这里大家应该明白了吧。实战这里服务器我选择使用nginx,来配置。服务端来进行压缩对于服务端来进行压缩,客户端啥也不用做,只需要把打好的包放入对应的目录下面,然后在访问的时候 nginx 自动进行压缩传给客户端进行解析等。nginx配置使用HttpGzip(这个模块支持在线实时压缩输出数据流)模块.下面这一段命令的作用域是 : http, server, location, 意思是在 http, server, location 这三个地方加入到哪个地方都行,为了不影响其他的,个人建议加到 location模块,这样其他的就不会影响了。 gzip on; gzip_buffers 4 16k; gzip_comp_level 6; gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php;命令意义gzip on开启或者关闭gzip模块gzip_buffers 4 16k设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流gzip_comp_level 6gzip压缩比,1 压缩比最小处理速度最快gzip_types text/plain application/javascript匹配MIME类型进行压缩,(无论是否指定)"text/html"类型总是会被压缩的效果主要加载的是这个应用在客户压缩在客户的压缩工具也有很多,这里我就介绍webpack 和 vite 客户端怎么进行压缩然后部署webpack大众使用的是这个工具,在压缩的时候,使用上面提到的那个压缩插件compression-webpack-plugin。然后在 vue.config.js 或者 webpack.config.js 里面加入插件的配置信息。// 这里使用的 vue.config.js, webpack.config.js 里面内容大部分相同,只是vue.config.js里面是链式调用的。 const compressionWebpackPlugin = require('compression-webpack-plugin') configureWebpack: { plugins: [new compressionWebpackPlugin({ filename: '[path].gz[query]', //压缩后的文件名 algorithm: 'gzip', // 压缩格式 有:gzip、brotliCompress, test: /\.(js|css|svg)$/, threshold: 10240,// 只处理比这个值大的资源,按字节算 minRatio: 0.8, //只有压缩率比这个值小的文件才会被处理,压缩率=压缩大小/原始大小,如果压缩后和原始文件大小没有太大区别,就不用压缩 deleteOriginalAssets: false //是否删除原文件,最好不删除,服务器会自动优先返回同名的.gzip资源,如果找不到还可以拿原始文件 })],效果如下:vite本人项目是使用vite来进行构建的这里也需要安装一个插件, 一开始我以为是 rollup-plugin-gzip 后面发现不对,vite 自己做了一个插件出来。vite-plugin-compression 使用方式很简单import viteCompression from 'vite-plugin-compression'; plugins: [ viteCompression() ],效果如下:nginx 配置没错,这里nginx 也要配置, 配置启动gzip模块, 然后优先使用本地压缩好的文件。 gzip_static on; gzip_http_version 1.1; gzip_proxied expired no-cache no-store private auth;命令作用gzip_static on启动模块。您应该确保压缩和解压文件的时间戳匹配gzip_http_version版本,默认是1.1, 使用 gzip_static,就是要 1.1的版本gzip_proxiedNginx作为反向代理的时候启用,开启或者关闭后端服务器返回的结果效果引用https://segmentfault.com/a/1190000012800222解释的很到位http://nginx.org/ru/docs/http/ngx_http_gzip_module.html nginx 英文文档https://www.nginx.cn/doc/standard/httpgzip.html 中文文档https://blog.csdn.net/weixin_41277748 感谢这个小伙伴,昨晚一晚上的商量
最近在服务器迁移,安装node环境也遇到些问题,本来想着安装个低版本的,但是一升级 npm, 就报错了 npm WARN npm npm does not support Node.js v8.11.1, 错误的意思很明显是 npm 在node 的版本不支持。所以没办法,自己搞,先卸载,后安装。卸载yum remove nodejs npm -y 使用这个命令来卸载,会发现没有用结果:既然卸载不了,那我来删除文件总可以吧。使用命令 rm -rf 删除node 的文件夹和软连接(没有软连接的可以不用删除)删除完后就使用 node-v 会包命令不存在安装我安装软件一般在 /usr/local 这个目录下面,所以 先使用 cd /usr/local 去执行下面的命令。wget https://nodejs.org/dist/v16.4.0/node-v16.4.0-linux-x64.tar.xz : 下载node的包,要注意的是,需要把 16.4.0 -----> 换成比较新的包,我目前使用的是最新版本的,但是后面可能就不是最新版本了。xz -d node-v16.4.0-linux-x64.tar.xz: 把.xz的包 -----> 解压成 .tar的包tar xf node-v16.4.0-linux-x64.tar:把.tar的包 -----> 解压成 文件夹cp -r node-v16.4.0-linux-x64 /usr/local/: 移动目录到usr/local 目录下面(可选,安装自己安装包的习惯,这里这么做也为了下面的软连接做准备)ln -s /usr/local/node-v16.4.0-linux-x64/bin/node /usr/local/bin/node: 配置node的软连接ln -s /usr/local/node-v16.4.0-linux-x64/bin/npm /usr/local/bin/npm: 配置npm的软连接ln -s /usr/local/node-v16.4.0-linux-x64/bin/npx /usr/local/bin/npx: 配置npx的软连接接下来你使用 node-v, npm -v结果如下:看到这个就是 node 安装好了,npm 也是按照好了的,并且可以全局使用哦安装 pm2 来守护进程使用命令 npm install -g pm2 全局安装 pm2, 安装好了后, 配置软连接来在命令行中生效。ln -s /usr/local/node-v16.4.0-linux-x64/bin/pm2 /usr/local/bin/pm2: 配置pm2的软连接如下结果就是安装好了的:pm2 list :查看进程pm2 start index.js :启动某个node服务pm2 restart dev-server --name newname 带名称启动服务pm2 stop/reload/restart/delete all :停止/重载/重启/删除 所有进程pm2 stop/reload/restart/delete 0 :停止/重载/重启/删除 pm2进程列表中进程为0的进程pm2 logs [--raw] :显示所有进程的日志pm2 flush :清空所有日志文件pm2 reloadLogs :重载所有日志等你启动了进程后就可以查看列表了
本人在使用 node + koa来实现gitup的授权登录,但是在第二步获取access_token的时候报下面的错误了。关于gitup如何授权登录,有兴趣的同学可以百度一下,获取访问官网的api地址 https://docs.github.com/cn/developers/apps/building-oauth-apps/authorizing-oauth-apps#%E5%93%8D%E5%BA%94,这里面讲的很详细,本人就不在赘述了,要说的是在获取access_token时候遇到的问题,在这里看看能不能帮到有缘人。问题复现报的是这个错误,啥意思,从错误的名称来看,是连接重置了,但是连接为啥会重置呢?gitup问题gitup 在国内有一个问题——打不开,然后此时,好多博客或者文章就会让你去使用 科学上网的工具,科学上网的工具呢,本身是走的是一个代理,通过代理来实现gitup可以访问。关于科学上网的工具请自行搜索。第二步获取access_token获取第二步的代码很简单,如下: async gitUpCallBack(ctx: Application.ParameterizedContext) { // 获取之前的code const { code } = ctx.request.query as ILoginGitUpCallbackParams; // 获取到code后,请求gitup服务获取access_token try { const accessToken = await axios({ method: 'post', url: 'https://github.com/login/oauth/access_token?client_id=client_id&client_secret=client_secret&code=' + code, headers: { 'User-Agent': 'blog web' console.log(accessToken, '-----accessToken'); } catch (err) { console.log(err, '-----err'); ctx.body = '获取成功!' }上面的代码是一个koa的中间件,然后使用 axios来发送http请求。错误排查对于出现错误,我们最主要的是进行错误排查,我一开定位的是 axios是不是不能发送post请求,然后我使用原生的 http服务来发送,也是报相同错误的。使用postman进行测试对于同样的参数,我们使用postman进行测试,发现是好的,能够获取预期的结果(access_token)如下:然后使用postman获取node端的源码,发现和我们的是一样的,我们的代码是没有问题的。寻找结果我发现好多错误都类似,并且听到了 proxy 这个词,那我就想到我本身也是有代理的,postman可以请求成功是没有走代理的,所以最后关闭代理进行测试。(下面抽取两个简单的截屏)关掉科学上网后会发现,我们的gitup已经打不开了,这里提供一个暂时的解决办法,修改hosts配置文件如下进入gitup我测试的时候是这么做的,1.找一个ping gitup.com 延迟比较低的ip,寻找的方法是,输入网址:http://ping.chinaz.com/github.com2.打开C:\Windows\System32\drivers\etc 目录下面,找到我们的hosts文件,然后进行修改保存后,就可以打开gitup了,但是有一个缺陷,短暂时间可以用,但是长时间不行,需要频繁的进行修改,目前我还没有办法怎么长时间的访问gitup不挂,除了科学上网外测试结果获取成功,大功告成!
最近博客写道项目列表中,发现这里比较多图片,一开加载会比较慢,然后就想要用一个loading的图片来占位。与此同时,如果图片加载失败那么显示错误的图片,不显示一个原有的错误,那样比较难看。效果原理解析这个就是一个组件,一个图片展示的组件,直接更改img标签的url地址就好,对的,是这样的,在vue中直接更改地址,vue会有响应式的更新数据。图片的事件图片是有许多的事件的,例如,onload, onerror等,图片只要一加载就会调用onload的事件,不管是加载成功还是加载失败都会调用这个方法。而onerror方法是图片在没有显示出来就会调用这个方法。从这两个方法对比可以得知,我们需要使用onload来一开始加载图片,并且图片可以成功,可以失败等。组件代码import { ImgHTMLAttributes } from "react"; * 图片占位组件属性 export interface IImagProps<T> extends ImgHTMLAttributes<T> { * 加载中的图片 loadingImg?: string, * 失败加载的图片 errorImg?: string, * 图片正常显示的地址 src: string, import React, { useState } from 'react' // 下面这两个是导入默认的图片 import loadImg from './../../../assets/imgs/loading/load.gif'; import errorImg from './../../../assets/imgs/loading/error.png' export default function Img(props: IImagProps<any>) { // 图片地址 const [src, setSrc] = useState(props.loadingImg as string) // 是否第一次加载,如果不使用这个会加载两次 const [isFlag, setIsFlag] = useState(false) * 图片加载完成 const handleOnLoad = () => { // 判断是否第一次加载 if (isFlag) return; // 创建一个img标签 const imgDom = new Image(); imgDom.src = props.src; // 图片加载完成使用正常的图片 imgDom.onload = function () { setIsFlag(true) setSrc(props.src) // 图片加载失败使用图片占位符 imgDom.onerror = function () { setIsFlag(true) setSrc(props.errorImg as string) return ( <> <img src={src} onLoad={handleOnLoad} style={{ height: 'inherit', ></img> </> // 设置默认的图片加载中的样式和失败的图片 Img.defaultProps = { loadingImg: loadImg, errorImg: errorImg
最近在使用vite+react + ant-design 来搭建个人站点,看到网上好多网站都实现了黑白皮肤的切换,并且ant-design帮我们实现了三套主题色,一个默认亮白色,暗黑主题和紧凑主题。于是我也想来弄一弄。最后还是实现了,打包后也是ok的。效果思路对于网站需要切换主题的话,一般有以下几种办法。使用css覆盖的方式,由于css基于后面的css覆盖前面的原理,所以这一点也是可以的。但是这一点对于使用less和scss的码友来说,貌似不是一个很好的方法由于less里面带有一个less.js的cdn,可以用来解析是html种使用less文件,但是这个需要注意使用的顺序,需要less的样式文件在前面,less.js的引用在后面,这个对于使用构建工具的同志来说不太友好,打包后less文件都不见了,直接使用路径肯定也是不行。社区成熟的两个库: antd-theme-webpack-plugin和antd-theme-generator,对于我的项目来说貌似都不怎么合适,首先:antd-theme-webpack-plugin这个库是基于webpack来的,我们都知道vite是在开发环境使用esbuild,生产环境使用的是roallup来进行打包。antd-theme-generator这个库的话,把less提升到了运行阶段,我们代码一般会进行打包压缩等,如果使用这个库的话就意味着需要配置less相关的静态资源不能被打包,不然会有问题。基于上面的思路我做了以下的方法来进行尝试。我的代码地址是(这个地址不会改动): https://github.com/cll123456/blog项目做了以下调整:将ant-design的两个主题,默认主题和暗黑主题引入到我自己的less文件中。然后对此就可以后序实现改动主题色,例如:成功,失败,警告等。如下:这个引入的顺序需要注意,后引入的变量会覆盖前面的。不会自定义的会不生效在我点击switch框的时候触发方法。做以下尝试所有的尝试都是基于下面的第二步,也就是方法,这里面需要做啥事情尝试一改变方法的时候直接来动态引入less文件,这样在引入暗黑主题是可以实现的,但是从暗黑主题却切换不过来了。如下:const handleSkin = (checked: boolean) => { if (checked) { // 明亮主题 import('./../assets/style/index.less') } else { // 暗色主题 import('./../assets/style/index.dark.less') }这个从白的可以切换到黑的原因是,黑色样式覆盖了前面白色的样式,但是如果你再一次覆盖却不行,我估计是选择器权重问题上,ant官方做了改动。如果需要从新切换回来也是有办法的,在明亮主题中直接window.location.reload(),这样是可以切换回来的,如下图:这样虽然实现了功能,体验肯定是不好的,作为一名前端工程师,肯定是需要非常注重体验的,不然职业生涯的路可能就不会很长。尝试二由于尝试一不行,然后我就往import动态引入这边考虑了,我考虑的方向是既然可以动态import引入,那么我可以再一次改变的时候把前一次引入的给remove掉么?但是我找遍了所有的文件,import导入的是无法remove掉的,import导入是现代浏览器里面的esm的语法。然后就去网上找各种方法,在ant-design pro中发现实现了这个功能,并且是无刷新的,然后就去gitup上看人家的源码。功夫不负有心人,然后发现人家是动态使用link引入css的方式来实现的,那么我也可以来通过link导入less文件来实现,并且使用less.js的cdn来进行解析。添加一个addSkin的方法,毕竟需要导入文件,然后来查找原来是否存在,然后进行删除。// 调用方法 const handleSkin = (checked: boolean) => { if (checked) { // 明亮主题 addSkin("./../../src/assets/style/index.less") } else { // 暗色主题 addSkin("./../../src/assets/style/index.dark.less") // 添加皮肤的方法 function addSkin(path: string) { let head = document.getElementsByTagName("head")[0]; const getLink = head.getElementsByTagName('link'); // 查找link是否存在,存在的话需要删除dom if (getLink.length > 0) { for (let i = 0, l = getLink.length; i < l; i++) { if (getLink[i].getAttribute('data-type') === 'theme') { getLink[i].remove(); // 查找script是否存在 const getScript = head.getElementsByTagName('script'); if (getScript.length > 0) { for (let i = 0, l = getScript.length; i < l; i++) { if (getScript[i].getAttribute('data-type') === 'theme') { getScript[i].remove(); // 最后加入对应的主题和加载less的js文件 let link = document.createElement("link"); link.dataset.type = "theme"; link.href = path; link.rel = "stylesheet"; link.type = "text/css"; head.appendChild(link); // 这个less.js一定要放到后面才行 let script = document.createElement('script'); script.type = 'text/javascript'; script.dataset.type = 'theme'; script.src = 'https://cdn.bootcdn.net/ajax/libs/less.js/4.1.1/less.js' head.appendChild(script) }这种方法是动态改变link标签的样式来实现的,在生产环境是没有任何问题,但是在开发环境就不行了,打包后路径不存在。肯定是不行的,接下来我就去找vite如何静态资源复制到打包的文件,方法找到了。但是我的less里面引用了antd里面的less,里面的也不用打包? 我觉得不太好,因此再一次放弃。尝试三既然直接使用less文件不行,那我可以使用css不,和ant-design pro里面一样的,我也来引用css文件,接下来就往这个方向。我直接打印了,import dark from './xxxx'.less 发现既然是一个字符串。是编译好的字符串,那我直接使用style标签就好了。说干就往下干。import dark from './../assets/style/index.dark.less' import lighter from './../assets/style/index.less' // 调用方法 const handleSkin = (checked: boolean) => { if (checked) { // 明亮主题 addSkin(lighter) } else { // 暗色主题 addSkin(dark) // 添加皮肤的方法 function addSkin(content: string) { let head = document.getElementsByTagName("head")[0]; const getStyle = head.getElementsByTagName('style'); // 查找style是否存在,存在的话需要删除dom if (getStyle.length > 0) { for (let i = 0, l = getStyle.length; i < l; i++) { if (getStyle[i].getAttribute('data-type') === 'theme') { getStyle[i].remove(); // 最后加入对应的主题和加载less的js文件 let styleDom = document.createElement("style"); styleDom.dataset.type = "theme"; styleDom.innerHTML = content; head.appendChild(styleDom); }这里有一个细节就是,样式导入必须在顶部导入,不然vite会检测不到,不能使用动态导入,打包会经过treeshake去掉.其实这里还有一个问题,那就是css打包后会比较大,毕竟引入了两份,这个问题就留给码友了,自己去vite获取其他的构建工具(webpack, gulp等)上找静态资源太大怎么处理。总结在真实的调试中肯定是不止这三遍尝试的,这里只记录走向成功的关键三步。More interest, less interests (多一些兴趣爱好的向往,少一些功名利禄的追求)
这个hook比较简单,作用: 获取函数组件里面的事件,我们通过 ref 来获取类组件的事件,所以 这个 useImperativeHandle Hook 一般是于 ref 转发一起使用。语法useImperativeHandle(ref, createHandle, [deps])参数一 ref: 子组件中 ref转发传过来的 ref参数二 createHandle: 子组件需要对外的事件,通过一个函数返回对象定义参数三deps: 依赖,如果依赖变化,则会重新调用案例获取类组件中的事件获取类组件的事件,就是需要获取类组件的实例对象,然而获取实例对象,通过ref 来就行。import React, { PureComponent, Ref, useCallback } from 'react' class TestGetClassHandle extends PureComponent { // 测试的事件 testHandle = () => { console.log('获取类组件中的事件'); render() { return ( <div> <h1>类组件</h1> </div> export default function TestImperativeHook() { // 创建一个 ref const classRef: Ref<TestGetClassHandle> = React.createRef(); // 获取子组件的事件 const handle = useCallback(() => { classRef.current!.testHandle() }, []) return ( <div> <TestGetClassHandle ref={classRef}></TestGetClassHandle> <button onClick={handle}>获取子组件事件</button> </div> }类组件效果函数组件的事件首先通过 ref 转发,然后在通过useImperativeHandle Hook获取函数组件中的事件import React, { Ref, useCallback, useImperativeHandle } from 'react' // 定义接口类型 interface IR { testHandle: () => void function TestGetFuncHandle(props: {}, ref: Ref<IR>) { useImperativeHandle(ref, () => ({ // 需要把函数组件对外暴露的的事件写到这里 testHandle: handle const handle = () => { console.log('函数组件的事件调用了'); return ( <> <div >函数组件</div> <button onClick={ handle}>组件自己调用</button> </> const NewTestFor = React.forwardRef(TestGetFuncHandle); export default function TestImperativeHook() { const funcRef: Ref<IR> = React.createRef() // 获取子组件的事件 const handle = useCallback(() => { funcRef.current!.testHandle() }, []) return ( <div> <NewTestFor ref={funcRef}></NewTestFor> <button onClick={handle}>获取子组件事件</button> </div> }函数组件调用效果
下拉树 就是一个下拉框里面的options里面换成一棵树的形状。本人业务需要一个这样的组件,我也懒得去发布一个组件到npm库,毕竟现在vue3出来了,这个组件只适合vue2 并且是element ui的基础,限制条件有点多。所以在这里做个笔记,有需要的自己copy 代码到自己本地,就行。效果下面的底色不要在意哈使用方式模板文件 <select-tree :value="test" :options="options" :props="{ value: 'id', // ID字段名 label: 'label', // 显示名称 children: 'children' // 子级字段名 }" :filterable="true" />数据看到这个数据,肯定明白,组件已经支持了数据回显。 test: '1-1', // 测试树数据 options: [ label: '一级 1', id: '1', children: [{ label: '二级 1-1', id: '1-1', children: [{ id: '1-1-1', label: '三级 1-1-1' label: '一级 2', id: '2', children: [{ label: '二级 2-1', id: '2-1', children: [{ id: '2-1-1', label: '三级 2-1-1' id: '2-2', label: '二级 2-2', children: [{ id: '2-2-1', label: '三级 2-2-1' }]组件代码组件名称 SelectTree.vue, 如果需要加上disabled 或者个性化的, 自己可以手动添加,代码已经有注释了<template> <div class="tree_select"> <el-select :value="valueTitle" ref="selectEl" :filterable="filterable" :clearable="clearable" @clear="clearHandle" :filter-method="selectFilterData" :size="size"> <el-option :value="valueId" :label="valueTitle"> <el-tree id="tree-option" ref="selectTree" :accordion="accordion" :data="options" :props="props" :node-key="props.value" :expand-on-click-node="false" :default-expanded-keys="defaultExpandedKey" :filter-node-method="filterNode" @node-click="handleNodeClick"> </el-tree> </el-option> </el-select> </div> </template> <script> export default { name: 'SelectTree', props: { /* 配置项 */ props: { type: Object, default: () => { return { value: 'id', // ID字段名 label: 'title', // 显示名称 children: 'children' // 子级字段名 /* 选项列表数据(树形结构的对象数组) */ options: { type: Array, default: () => { return [] /* 初始值 */ value: { type: [Number, String], default: () => { return null /* 可清空选项 */ clearable: { type: Boolean, default: () => { return true /* 自动收起 */ accordion: { type: Boolean, default: () => { return false * 下拉选项框的大小,默认最小 size: { type: String, default: () => { return 'mini' // 是否可以搜索 filterable: Boolean data () { return { valueId: this.value, // 初始值 valueTitle: '', defaultExpandedKey: [] mounted () { this.$nextTick(function () { this.initHandle() methods: { // 初始化值 initHandle () { if (this.valueId) { this.valueTitle = this.$refs.selectTree.getNode(this.valueId).data[this.props.label] // 初始化显示 this.$refs.selectTree.setCurrentKey(this.valueId) // 设置默认选中 this.defaultExpandedKey = [this.valueId] // 设置默认展开 this.$nextTick(() => { const scrollWrap = document.querySelectorAll('.el-scrollbar .el-select-dropdown__wrap')[0] const scrollBar = document.querySelectorAll('.el-scrollbar .el-scrollbar__bar') scrollWrap.style.cssText = 'margin: 0px; max-height: none; overflow: hidden;' scrollBar.forEach(ele => { ele.style.width = 0 // 切换选项 handleNodeClick (node) { this.valueTitle = node[this.props.label] this.valueId = node[this.props.value] this.$emit('getValue', this.valueId) this.defaultExpandedKey = [] // 选中后失去焦点,隐藏下拉框 this.$refs.selectEl.blur() // 把数据还原 this.selectFilterData('') * 下拉框搜索 selectFilterData (val) { this.$refs.selectTree.filter(val) * 过滤节点 filterNode (value, data) { if (!value) return true return data.label.indexOf(value) !== -1 // 清除选中 clearHandle () { this.valueTitle = '' this.valueId = null this.defaultExpandedKey = [] this.clearSelected() this.$emit('getValue', null) /* 清空选中样式 */ clearSelected () { const allNode = document.querySelectorAll('#tree-option .el-tree-node') allNode.forEach((element) => element.classList.remove('is-current')) watch: { * 监听绑定的值变化 value () { this.valueId = this.value this.initHandle() </script> <style lang="scss" scoped> ::v-deep .el-select .el-input .el-input__inner { color: #fff !important; .el-scrollbar .el-scrollbar__view .el-select-dropdown__item { height: auto; max-height: 274px; padding: 0; overflow: hidden; overflow-y: auto; .el-select-dropdown__item.selected { font-weight: normal; ul li > > > .el-tree .el-tree-node__content { height: auto; padding: 0 20px; ::v-deep .el-tree-node__content:hover, ::v-deep .el-tree-node__content:active, ::v-deep .is-current > div:first-child, ::v-deep .el-tree-node__content:focus { background-color: #F5F7FA; color: #409EFF; </style>
含义从标题上看,自定义hook的主要是自己定义,那么对于hooks的定义又是啥呢? 简单点的回答,hooks是一个函数,并且是在react 函数组件中使用的,不同的hook的作用也是不一样的,例如,state hook是用来定义函数组件的状态, 而effect hook 是用来定义组件的副作用,那么自定义hook是用来干啥的呢?,自定来定义一个hook 函数,里面可以包含 多个hooks。简单点的说是,把相同逻辑的hooks封装在同一个函数里。规则1.hooks 的使用方式都是 use 开头,那么我们自己定义的也用这个use 开头,作为是一个hook 的标记,统一开发规范2.自定义的hooks 肯定也是一个函数,并且需要和react 给我们的组件一样,需要放到顶层作用自定义hooks的作用让代码更加简洁,就是对代码逻辑块的封装。而hooks的作用主要是横切关注点来处理问题。处理自定义hooks可以处理横切关注点外,还有高阶组件和props render 也是来做这个事情的。案例在这里我们想要实现一个这样的功能,做一个每隔1s来轮询后台的方法,并且在使用一个开关,想要轮询打开就行,不想要直接关闭。下面使用两种方式来实现,一种是自定义hook的方式,另一种是 函数组件的高阶组件也可以实现这样的效果自定义hook 案例源码import React, { useEffect, useState } from 'react' * 列表组件 * @returns function ListComp() { const { count, data } = useTimerReqHooks(); const liDom = data.map((it, index) => (<li key={index}>{it}</li>)); return ( <> <p>次数: {count}</p> <p>数据</p> <ul> {liDom} </ul> </>) * 测试组件 * @returns export default function TestCusHook() { const [hasShow, setHasShow] = useState(true); return ( <div> <p><button onClick={() => { setHasShow(!hasShow) }}>隐藏/显示</button></p> {hasShow && <ListComp />} </div> * 自定义hook,做一个轮询后台的处理,每隔1s钟发一次请求(实际的请求不可能这么频繁) export function useTimerReqHooks() { // 计时器记录的数据 const [count, setCount] = useState<number>(0); // 时间变化请求 const [data, setData] = useState<number[]>([]); // 计数器 let timer: number | null = null; // 副作用,发起请求 useEffect(() => { timer = setInterval(() => { // 计时器值 + 1 setCount(pre => pre + 1); // 这里用一个立即执行函数来发送请求 (async () => { const res = await getData(1, 10); console.log(data, res, '-====='); // 如果想要拿到先前的数据,需要返回一个函数,不然做不到,异步的 setData(pre => [...pre, res]); }, 1000) return () => { // 清空定时器 if (timer) { clearInterval(timer); timer = null; }, [count]); return { data, count * 模拟发送请求,每一次返回一个随机数 function getData(min: number, max: number): Promise<number> { return new Promise((resolve, reject) => { resolve(parseInt((Math.random() * (max - min)).toString(), 10) + min) }自定义hook 效果组件树是纯粹的,没有添加任何的其他组件,方便调试高阶组件import React, { PureComponent } from 'react' * @param comp 高阶组件的接口 * @returns interface IWithTimerReqS { // 当前次数 count: number, // 获取的结果 data: number[] * 显示组件 interface ITestCusCompS extends Partial<IWithTimerReqS> { // 是否展示组件 hasShow: Boolean, * 高阶组件 function withTimerReq(Comp: React.ComponentClass<IWithTimerReqS>) { return class withTimerReqs extends PureComponent<{}, IWithTimerReqS> { state: IWithTimerReqS = { count: 0, data: [], private timer: number | null = null; // 组件初始化的时候进行启动定时器 componentDidMount() { this.setData(); // 数据更新进行操作 componentDidUpdate(prevProps: {}, prevState: IWithTimerReqS) { this.setData(); private setData(){ if(this.timer){ clearInterval(this.timer); this.timer = null; // 启动定时器 this.timer = setInterval(() => { // 计时器值 + 1 this.setState(pre => { return { ...pre, count: pre.count + 1 // 这里用一个立即执行函数来发送请求 (async () => { const res = await getData(1, 10); console.log(this.state.data, res, '-====='); this.setState(pre =>{ return { ...pre, data: [...pre.data, res] }, 1000) // 组件卸载,清空定时器 componentWillUnmount() { if(this.timer){ clearInterval(this.timer); this.timer = null; render() { return (<> <Comp {...this.state} /> </>) * 列表组件 class ListComp extends PureComponent<IWithTimerReqS> { render(){ const liDom = this.props.data.map((it, index) => (<li key={index}>{it}</li>)); return ( <> <p>次数: {this.props.count}</p> <p>数据</p> <ul> {liDom} </ul> </> // 使用高阶组件包裹 const WithComp = withTimerReq(ListComp) export default class TestCusComp extends PureComponent<{}, ITestCusCompS> { state: ITestCusCompS = { hasShow: true render() { return ( <div> <p><button onClick={() => { this.setState({ hasShow: !this.state.hasShow }) }}>隐藏/显示</button></p> {this.state.hasShow && <WithComp />} </div> * 模拟发送请求,每一次返回一个随机数 function getData(min: number, max: number): Promise<number> { return new Promise((resolve, reject) => { resolve(parseInt((Math.random() * (max - min)).toString(), 10) + min) }高阶组件效果总结虽然两者实现的效果都是一样的,但是类组件中的代码量比hooks 多了大概50行左右,而且还比较绕。但是hooks 就比较纯粹,组件结果不增加,做到真正的横向关注点。
nprm 不是内部或外部命令,也不是可运行的程序“nprm 不是内部或外部命令,也不是可运行的程序, 看到这个错误,我们一下子就能明白,啥原因不能使用命令呢,那肯定就是环境变量没有配置啦。不管是哪个命令,报这个问题都是环境变量没有配置好。对了,简单介绍下,我出现这个问题的原因是,我的node 按照的是在D盘,我改变了我们的node环境的默认按照方式,在后面的按照过程也会出现许多的毛病,但是只要是自己的好奇心够大,那都是能解决的。解决 nprm 不是内部或外部命令打开环境变量,直接配置即可。如下图:为啥要配置到那个文件夹呢?也就是说,对于我的环境是要配置到 node_global 这个文件夹。对于看到同学的自己根据自己的实际情况来配置,反正就是配置环境变量。nrm报错 [ERR_INVALID_ARG_TYPE]配置好环境变量一打开就发现,我的命令找到了,但是却报了一个错误,如下:意思是说路径找不到,对应源码在:解决办法:// const NRMRC = path.join(process.env.HOME, '.nrmrc'); const NRMRC = path.join(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], '.nrmrc');对应为啥在node环境中,电脑明明是64位的,但是node的操作平台却是win32的。这个问题,个人的理解是:所以,不论是32位还是64位操作系统,process.platform的值只能是’win32’效果:
大家都知道,移动端喝pc端的区别在于的是,移动端怎么做适配。还有vue3 和 ts 如何进行结合。本人抱着more interest, less interests的目的。来记录自己在开发过程中的一些公共的方面。来帮助更多的技术人.效果作为移动端最关键的页面兼容问题,在本项目是已经解决了的。源码地址: https://github.com/cll123456/vue3-ts-mobile.git演示地址: https://cll123456.github.io/vue3-ts-mobile/#/安装依赖"browserslist": [ "defaults", // 默认 "last 2 versions", // 兼容主流浏览器的最近两个版本 "> 1%", // 使用的浏览器需要在市场上的份额大于1 "iOS 7", // ios 系统版本大于7 "last 3 iOS versions" // 兼容ios的最新3个版本 "dependencies": { // 生产依赖 "axios": "^0.21.1", // 发ajax 请求的包 "vant": "^3.0.9", // 安装vant ui 库 "vue": "^3.0.5", // vue3 的版本 "vue-router": "^4.0.4", // 对应vue3 的 路由版本 "vuex": "^4.0.0" // 对应vue3的vuex, 在这里给一个温馨提示。在vue3中,做数据的存储,其实可以不需要使用vuex, 例如: provide/ reject 、 全局ref(变量) 都是可以的,这个根据实际项目的情况来决定 "devDependencies": { // 开发黄金依赖 "@types/node": "^14.14.36", // node 环境的类型检查 "@vitejs/plugin-vue": "^1.1.5", // vite 的封装vue3的包, "@vue/compiler-sfc": "^3.0.5", // vue3 编译 .vue模板的包 "autoprefixer": "^10.2.4", // 自动添加 css 的前缀 "postcss-pxtorem": "^5.1.1", // 用于将px 转成rem 的包,在项目中就可以使用 px啦 "sass": "^1.32.8", // sass 只要按照就行,用于css 的工程化 "typescript": "^4.1.3", // ts "vite": "^2.0.5" // vite 工具 }主入口 vite.config.jsimport { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], base: './',//打包路径 // 别名 resolve: { alias: { '@': path.resolve(__dirname, './src')//设置别名 // 全局css css: { preprocessorOptions: { scss: { // 全局的scss ,跨域放多个,例如:主题的变量,和一些混合等 additionalData: `@import "./src/style/mixin.scss";`, // 代理服务 server: { port: 4000,//启动端口 // open: true, proxy: { // 第一个代理 '/api/mobile':{ // 匹配到啥来进行方向代理 target: 'https://github.com/cll123456/vue3-ts-mobile', // 代理的目标 rewrite: (path) => path.replace(/^\/api/, '') // 如果不需要api 直接把路径上的api 替换成空,这个 // 第二个代理 '/api/md': { target: 'https://editor.csdn.net/md?not_checkout=1&articleId=115252632',//代理网址 changeOrigin: true, // 支持跨域 rewrite: (path) => path.replace(/^\/api/, '') })ts 配置Vite支持直接导入.ts文件。Vite只对.ts文件执行翻译,不执行类型检查。它假设类型检查由IDE和构建过程负责(可以在构建脚本中运行tsc——noEmit)。Vite使用esbuild将TypeScript转换为JavaScript,比普通tsc快20~30倍,HMR更新可以在50毫秒内反映到浏览器中。请注意,因为esbuild只执行不带类型信息的转换,所以它不支持某些特性,如const enum和隐式的纯类型导入。你必须在tsconfig中设置"isolatedModules": true。这样TS就会对那些不能与单独的翻译一起工作的特性发出警告。 "compilerOptions": { // 编译选项 "target": "esnext", // ts的编译的目标es的版本 "module": "esnext", // 模块的版本是 esnext(下一阶段,) "moduleResolution": "node", // 模块的解析 node的模块解析方式 "strict": true, // 启动严格的代码检查 "jsx": "preserve", // 使用的jsx 是转化成怎么的表现形式 "sourceMap": true, // 打包后是否使用资源地图,方便查找问题所在 "resolveJsonModule": true, // 是否支持使用import 来导入json文件 "isolatedModules":true, // 每一个文件是否单独编译成一个文件,这个在开发阶段很重要,生产环境设置成false,因为vite是基于每一个文件的改变来进行热更新,如果不开启这个选项,ts 改变后不会自动热更新 "esModuleInterop": true, // 是否启动模块化与非模块化的文件的交互 "lib": ["esnext", "dom"], // 环境, dom 环境 和 最新的es "types": ["vite/client"] //Vite应用程序中缓冲客户端代码环境, 默认是node api "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] // 这是需要转换的代码目录和文件后缀名解决页面大小兼容问题新建立postcss.configmodule.exports = { plugins: { autoprefixer: {}, 'postcss-pxtorem': { // 数字|函数)表示根元素字体大小或根据input参数返回根元素字体大小 rootValue: 37.5, // 使用通配符*启用所有属性 propList: ['*'], // 允许在媒体查询中转换px mediaQuery: true, // 过滤掉.norem-开头的class,不进行rem转换 selectorBlackList: ['.norem'] };建立 rem.ts,用于根据当前的窗口来自动改变跟的font-size// rem等比适配配置文件 // 基准大小, 这个是由于vantui的基准大小就是37.5 const baseSize = 37.5 // 注意此值要与 postcss.config.js 文件中的 rootValue保持一致 // 设置 rem 函数 function setRem() { // 当前页面宽度相对于 375宽的缩放比例,可根据自己需要修改,一般设计稿都是宽750(图方便可以拿到设计图后改过来)。 const scale = document.documentElement.clientWidth / 375 // 设置页面根节点字体大小(“Math.min(scale, 2)” 指最高放大比例为2,可根据实际业务需求调整) document.documentElement.style.fontSize = baseSize * Math.min(scale, 2) + 'px' // 初始化 setRem() // 改变窗口大小时重新设置 rem window.onresize = function () { setRem() export {}初始化样式style 中建立 reset.scss@charset "UTF-8"; /* stylelint-enable */ /* 重置样式 */ -webkit-tap-highlight-color: transparent; outline: 0; body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin: 0; padding: 0; vertical-align: baseline; img { border: 0 none; vertical-align: top; i, em { font-style: normal; ol, ul { list-style: none; input, select, button, h1, h2, h3, h4, h5, h6 { font-size: 100%; font-family: inherit; table { border-collapse: collapse; border-spacing: 0; a, a:visited { text-decoration: none; color: #333; body { margin: 0 auto; background: #e8e8ed; font-size: 14px; font-family: -apple-system,Helvetica,sans-serif; line-height: 1.5; color: #666; -webkit-text-size-adjust: 100% !important; /*-webkit-user-select: none; user-select: none;*/ }使用路由 router由于vue3的整改,路由的使用方式也发生了一些改变,具体查看官网import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; const routes: Array<RouteRecordRaw> = [ path: "/", name: "Home", component: () => import("../views/Home/index.vue"), // 使用懒加载 const router = createRouter({ history: createWebHashHistory(), routes export default router;mian.ts 的结合import { createApp } from 'vue' import App from './App.vue' // 重置样式 import './style/reset.scss' import 'vant/lib/index.css'; // 全局引入样式 // 引入 更改跟节点的size import './rem' import Vant from 'vant'; // 挂载路由 import router from "./router"; createApp(App) .use(Vant) .use(router) .mount('#app')
含义错误边界: 用来捕捉错误的代码,说到捕捉错误。大家可能都会说 直接try catch 不就行了。对的,try catch 确实是一种在各个语言比较通用的方法。但是在react 组件中,如果某一个组件发生错误,他是会往他的父级组件抛出错误的,然后自己是会被卸载的。如果到跟组件都不能够处理错误,这个组件树就会被卸载,组件树卸载导致的页面效果就是直接的报错。默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播,防止组件树卸载,导致页面啥也没有使用方式让某个组件捕获错误方式一: getDerivedStateFromError1.编写生命周期函数 getDerivedStateFromError 1.静态函数 2.运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前 3.注意:只有子组件发生错误,才会运行该函数 4.该函数返回一个对象,React会将该对象的属性覆盖掉当前组件的state,使用的是混合,可以理解使用this.setState({}) 5.参数:错误对象 6.通常,该函数用于改变状态样例代码import React, { ComponentClass, PureComponent } from 'react' // 捕捉错误的状态 interface IGetErrorCompS { // 是否有错误 hasError: boolean, interface IProp { // 传入的组件 children: JSX.Element * 捕捉错误的组件 class GetErrorComp extends PureComponent<IProp, IGetErrorCompS> { state = { hasError: false * 该方法的执行时间是在子组件发生错误后,更新页面前触发的,返回一个对象来更新状态 * @returns static getDerivedStateFromError() { console.log('我是组件GetErrorComp组件,来捕捉我子组件的错误'); return { hasError: true render() { return ( <> {this.state.hasError ? '我捕捉到子组件的错误' : this.props.children} </> * 组件A class CompA extends PureComponent { render() { return ( <> <h1>我是组件CompA</h1> <GetErrorComp> <CompB /> </GetErrorComp> </> * 组件B class CompB extends PureComponent { render() { throw Error('我是CompB组件,我要报错') return ( <> <h1>我是组件CompB</h1> </> * 测试边界组件 export default class TestCompErrorBoundary extends PureComponent { render() { return ( <div> <h1>我是跟组件</h1> <CompA /> </div> 效果方式二:componentDidCatch2.编写生命周期函数 componentDidCatch 1.实例方法 2.运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态 3.通常,该函数用于记录错误消息代码修改效果注意细节某些错误,错误边界组件无法捕获1.自身的错误2.异步的错误3.事件中的错误总结:错误边界仅处理渲染子组件期间的同步错误
含义render props 术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术简单点说,就是用来关注功能一样,ui界面不一样 横向关注点。 这个其实不是一个新的知识,是在js 灵活继承上进行拓展的。举个例子官网举得那个例子蛮不错的,有兴趣的可以看一下我举一个这样的场景,在编辑用户,我们需要一个用户数据的表单,然后在个人中心的时候,我们也需要一个用户的表单,但是这两个界面长的是布局啥的都不一样,无法达到界面复用。功能都是展示用户数据。原理样例代码import React, { PureComponent, ReactNode } from 'react' // 用户组件的属性 interface ICompRenderPropS { username: string, // 用户名 email: string, // 邮箱 address: string, // 地址 // 定义接口 interface IProp { children: (data: ICompRenderPropS) => ReactNode; * 处理用户逻辑的组件 class DealUserLogicComp extends PureComponent<IProp, ICompRenderPropS> { // 假设数据我已经获取到了 state = { username: 'aaaa', email: '123@abc.com', address: '北京市xxxx' handleSave = () => { // 发送请求 // 保存数据 alert('保存了数据哦') render() { return ( <div> // 这里直接使用默认的children属性,传入一个children是函数,并且把数据作为参数传递给其他界面不一样的组件 {this.props.children(this.state)} <button onClick={this.handleSave}>保存</button> </div> // 修改用户信息的界面 class UpdateUser extends PureComponent { // 不在里面直接使用函数的原因是,每一次调用组件的函数都是一个新的函数,比较浪费资源 handleUpdateUser = (data: ICompRenderPropS) => ( <div className='update-user'> <h1>修改用户信息</h1> <label > 用户名:<input type="text" defaultValue={data.username} /> </label> <label > 邮箱: <input type="text" defaultValue={data.email} /> </label> <label > 地址:<input type="text" defaultValue={data.address} /> </label> </div> render() { return ( <DealUserLogicComp> {this.handleUpdateUser} </DealUserLogicComp> // 注册用户的界面 class RegisterUser extends PureComponent { // 不在里面直接使用函数的原因是,每一次调用组件的函数都是一个新的函数,比较浪费资源 handleRegisterUser = (data: ICompRenderPropS) => ( <div className='update-user'> <h1>注册用户</h1> <div > 用户名:<input type="text" defaultValue={data.username} /> </div> <div > 邮箱: <input type="text" defaultValue={data.email} /> </div> <div > 地址:<input type="text" defaultValue={data.address} /> </div> </div> render() { return ( <DealUserLogicComp> {this.handleRegisterUser} </DealUserLogicComp> // 对外测试的代码 export default class TestComRenderProp extends PureComponent { render() { return ( <div> <UpdateUser/> <RegisterUser/> </div> 原理效果分析:上面我们实现了一个保存用户数据的功能,但是界面却分为两种,一种是修改用户信息,另一种是注册用户。 这里我们使用的思路是:直接在父组件使用一个默认的子组件 children 并且传递的是一个函数,这个是不是有点像上下文的写法。然后来实现功能.而我们 render props 的原理也是这样的,只是提供了一个 render 属性。传入的也是一个函数。render proprender props 具体修改代码如下:import React, { PureComponent, ReactNode } from 'react' * 处理用户逻辑的组件 class DealUserLogicComp extends PureComponent<IProp, ICompRenderPropS> { // 假设数据我已经获取到了 state = { username: 'aaaa', email: '123@abc.com', address: '北京市xxxx' handleSave = () => { // 发送请求 // 保存数据 alert('保存了数据哦') render() { return ( <div> // 这里就不是children属性了,而是render 属性,如果使用ts,需要手动定义属性的类型 {this.props.render(this.state)} <button onClick={this.handleSave}>保存</button> </div> class UpdateUser extends PureComponent { // 不在里面直接使用函数的原因是,每一次调用组件的函数都是一个新的函数,比较浪费资源 handleUpdateUser = (data: ICompRenderPropS) => ( <div className='update-user'> <h1>修改用户信息</h1> <label > 用户名:<input type="text" defaultValue={data.username} /> </label> <label > 邮箱: <input type="text" defaultValue={data.email} /> </label> <label > 地址:<input type="text" defaultValue={data.address} /> </label> </div> render() { return ( // 这里就使用 render 属性来调用,记得传递一个函数哦 <DealUserLogicComp render={this.handleUpdateUser} /> class RegisterUser extends PureComponent { // 不在里面直接使用函数的原因是,每一次调用组件的函数都是一个新的函数,比较浪费资源 handleRegisterUser = (data: ICompRenderPropS) => ( <div className='update-user'> <h1>注册用户</h1> <div > 用户名:<input type="text" defaultValue={data.username} /> </div> <div > 邮箱: <input type="text" defaultValue={data.email} /> </div> <div > 地址:<input type="text" defaultValue={data.address} /> </div> </div> render() { return ( // 这里就使用 render 属性来调用,记得传递一个函数哦 <DealUserLogicComp render={this.handleRegisterUser} /> export default class TestComRenderProp extends PureComponent { render() { return ( <div> <UpdateUser/> <RegisterUser/> </div> 效果分析,其实 render props 不是一个新的api,是基于js 的动态可拓展的基础上来进行延申出来的一种模式。功能一样,界面不一样的横向关注点. 有点网友要说,横向关注点我可以直接使用高阶组件 不就行了么,能想到这个的,确实厉害,高阶组件是对功能来进行拓展的,所以对界面拓展也是轻而易举的。高阶组件使用高阶组件进行拓展的代码: import React, { PureComponent, ReactNode } from 'react' // 用户组件的属性 interface ICompRenderPropS { username: string, // 用户名 email: string, // 邮箱 address: string, // 地址 // 定义接口 interface IProp { render: (data: ICompRenderPropS) => ReactNode; * 高阶组件 * @param Comp function WidthDealUserLogicHoc(Comp: React.ComponentClass<ICompRenderPropS>) { return class DealUserLogicComp extends PureComponent<{}, ICompRenderPropS>{ // 假设数据我已经获取到了 state = { username: 'aaaa', email: '123@abc.com', address: '北京市xxxx' * 保存数据的方法 handleSave = () => { // 发送请求 // 保存数据 alert('保存了数据哦') render() { return ( <> <Comp {...this.state} /> <button onClick={this.handleSave}>保存</button> </> // 修改用户的数据 class UpdateUser extends PureComponent<ICompRenderPropS> { render() { return ( <> <div className='update-user'> <h1>修改用户信息</h1> <label > 用户名:<input type="text" defaultValue={this.props.username} /> </label> <label > 邮箱: <input type="text" defaultValue={this.props.email} /> </label> <label > 地址:<input type="text" defaultValue={this.props.address} /> </label> </div> </> // 注册用户的组件 class RegisterUser extends PureComponent<ICompRenderPropS> { render() { return ( <> <div className='update-user'> <h1>注册用户</h1> <div > 用户名:<input type="text" defaultValue={this.props.username} /> </div> <div > 邮箱: <input type="text" defaultValue={this.props.email} /> </div> <div > 地址:<input type="text" defaultValue={this.props.address} /> </div> </div> </> // 使用方式 const WithUpdateUser = WidthDealUserLogicHoc(UpdateUser); const WithRegisterUser = WidthDealUserLogicHoc(RegisterUser); export default class TestComRenderPropHOC extends PureComponent { render() { return ( <div> <WithUpdateUser /> <WithRegisterUser /> </div> }效果分析: 不管使用哪种方式,我们都能实现功能。在这里不是秀操作,是想边学边留下足迹,感叹react 真的非常灵活
含义上下文(context): 是指一个组件里面包含所有子组件组成dom 的树,那么在这颗虚拟dom树中的环境,就称之为上下文。说到树这个概念,稍微提一笔,在树中,每一个节点我们都可以理解他是一颗树的根节点(起始节点)。react中的上下文的特点:1.当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享,如概念所说,子组件与父组件中都可以使用上下文的数据。2.如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性props),毕竟组件的数据都是都是一层一层往下传的,如果突然组件的数据来着祖先组件,这会给组件维护起来带来一定的麻烦。3.一般情况下,用于第三方组件(通用组件)4.在react 中,上下文分为两种形式的,一个是react版本小于16.0以前旧的上下文,另一个是16.3后面提出新的上下文旧版本上下文创建上下文只有类组件才可以创建上下文给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束。添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次render之后运行。使用上下文中的数据要求:1.如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型2.可以在组件的构造函数中,通过第二个参数,获取上下文数据也可从组件的context属性中获取 {this.context}3.在函数组件中,通过第二个参数,获取上下文数据原理样列import React, { Component } from 'react' import PropTypes from "prop-types"; // 类组件 interface ITestOldContextS { a: string, b: number // 子组件1 class ChildA extends Component { render() { return ( <div> <h2>我是子组件</h2> <ChildB></ChildB> </div> // 子组件2 class ChildB extends Component { static contextTypes = { a: PropTypes.string render() { return ( <div> <h3>我是子组件的子组件,我获取上下文中的数据:{this.context.a}</h3> </div> export default class TestOldContext extends Component<{}, ITestOldContextS> { state: ITestOldContextS = { a: '测试', // 规定上下文中的类型 static childContextTypes = { a: PropTypes.string, // 给上下文中放入指 getChildContext() { return { a: this.state.a render() { return ( <div> <h1>我是跟组件, 我自己的数据 a: {this.state.a} b:{this.state.b}</h1> <ChildA /> </div> 问一个问题? 上面的上下文设计有没有问题?答案是肯定有问题的,不然为啥会去掉呢.1.上下文的数据比较混乱,如果组件的层级很多,如果开发过程中遗留一点不经意的问题,那么对于维护的人员来说,那将是很痛苦的一件事情。2.子组件可不可以修改上下文中的数据, 这个答案也是肯定的,但是需要在父组件中提供一个修改的方法来进行修改,这个是js的优势3.存在严重的效率问题,并且容易导致滥用上下文的数据变化1.上下文中的数据不可以直接变化,最终都是通过状态改变2.在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据新版本的上下文对于这个版本的上下文,个人觉得有点像一个小型的vuex, 因为数据是被抽离出来了。然后在父组件和子组件是共享的,主要是在使用方式上,让人觉得遍历。创建上下文上下文是一个独立于组件的对象,该对象通过React.createContext(默认值)创建,返回的是一个包含两个属性的对象Provider属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个value属性,通过该属性,可以为其数据赋值注意: 同一个Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次,如果把provider提升到跟组件,那么这个是不是就是一个vuex?Consumer属性:使用者。Consumer是一个组件,它的子节点,是一个函数(它的props.children需要传递一个函数)使用上下文中的数据两种方法: 第一种是和旧版api差不都的使用方式;1.在类组件中,直接使用this.context获取上下文数据,要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象2.在函数组件中,需要使用Consumer来获取上下文数据
项目简介这是一个简单的vue2 和 typescript 的后台管理模板。在使用Vue 的过程中,许多的同学和我一样希望有一个简单一点的模板,不需要太多的内容,懒得去删除那些不符合我们业务逻辑的部分。由于本人业务需求需要兼容ie10(该项目已经完成了ie10的兼容),所以使用vue2和ts 来构建一个管理后台的框架,里面只有一个登录和主页,主页实现了菜单的跳转,面包屑等,和一些基本的功能。项目地址https://github.com/cll123456/vue2-ts-template.git演示地址: http://chenliangliang.top:9012/Login账号: 长度大于3小于50的字符串就可以 如: admin密码: 不能为空, 随便啥值效果:获取方式git clone https://github.com/cll123456/vue2-ts-template.git 获取项目npm install 安装对应的依赖包npm run dev 启动开发环境npm run build 打包成生产项目技术栈vue2 + typescript + elementui + router + axios + scss 等包依赖简介生成环境包"dependencies": { "@babel/polyfill": "^7.12.1", // 兼容ie10 的关键包,需要在main.ts的第一行导入哦 "axios": "^0.21.1", // 获取网络请求 "core-js": "^3.6.5", // 核心js库 "element-ui": "^2.15.1", // elementui 库 "js-cookie": "^2.2.1", // 使用cookie 进行存储数据 "normalize.css": "^8.0.1", // css 对项目的基本样式初始化 "path-to-regexp": "^6.2.0", // 将路径字符串(如/ user /:name)转换为正则表达式,匹配路由 "style-resources-loader": "^1.4.1", // 对样式资源的加载器 "vue": "^2.6.11", // 不介绍 "vue-class-component": "^7.2.3", // vue 类组件库 必备 "vue-property-decorator": "^9.1.2", // vue 类的装饰器 "vue-router": "^3.2.0", // router "vue-svgicon": "^3.2.9", // 使用的图标 "vuex": "^3.4.0", // 存储数据的vuex "vuex-module-decorators": "^1.0.1" // vuex 的类型检查 },开发环境包不解释,基本上都是一些自带的包,然后安装一些预编译的包。eslint , ts, @types等开发注意图标图标直接去阿里里面复制到对应的svg图标到,src -> icons->svg 即可,然后使用命令 npm run svg(这里已经配置好了对应的脚本启动) 会自动的全局导入图标router所有的路由都如果需要在菜单的右侧中显示,必须要要配置在layout组件的children中.如:(详细请查看源码)颜色变量默认我全局导入了两个变量文件,一个是variable.scss, 另一个是 mixin.scss, 需要啥颜色直接改里面的$mianColor 和 subColor, 包括可以定义elementui的主题颜色表单验证我也封装了一个表单验证器,可以直接在el-form-item 中的rule 导入对应的规则,即可,如:菜单权限控制因为没有后台支持,权限控制直接在matchRouteMenu 路由守卫进行匹配和存入数据 即可最后:基础的架子已经搭建好,只适合一些需要兼容ie项目的vue应用。毕竟vue3 不兼容ie嘛!如果有用,请给一个小星星哦!
先上效果我抽离出了一个scss 文件,如下:自己动手,把$xxx 变量改成自己对应的就行<style lang="scss"> // 修改弹出层激活的样式 .el-menu--popup > div:has(.is-active){ background-color: $subColor !important; border-left: 2px solid #ed1c24 !important; // 修改弹出层hover样式 .el-menu--popup > div:has(.el-menu-item) :hover{ background-color: $subColor !important; border-left: 2px solid #ed1c24 !important; color: $menuActiveText !important; // 添加虚线 .el-menu--popup> div { border-bottom: 1px dashed rgba(201, 211, 213, 0.7) !important; // 修改element菜单的样式 .el-menu { border: none; height: 100%; width: 100%; background-color: $menuBg !important; /**配色 */ .el-menu-item { background-color: $menuBg !important; color: $menuText !important; &:hover { background-color: $subColor !important; border-left: 2px solid #ed1c24 !important; color: $menuActiveText !important; &.is-active { background-color: $subColor !important; border-left: 2px solid #ed1c24 !important; color: $menuActiveText !important; .el-submenu__title { background-color: $menuBg !important; &:hover { background-color: $subColor !important; border-left: 2px solid #ed1c24; color: $menuActiveText !important; .el-submenu.is-active > .el-submenu__title { color: $menuText !important; &:hover, &:hover > i { color: $menuActiveText !important; // 一级菜单加上横向实线,二级菜单虚线 .submenu-title-noDropdown, .el-submenu__title { border-bottom: 1px solid rgba(201, 211, 213, 0.3) !important; // 二级菜单添加虚线 .el-menu--inline > div { border-bottom: 1px dashed rgba(201, 211, 213, 0.7) !important; </style>
ref 的基本使用方法我们在上一篇博客中讲了,但是在上一篇中还遗留了一个问题,就是函数组件,如果我想使用ref,这个怎么操作?在问一个问题,我们在函数组件中通过ref想获取啥?—— dom、react 对象?带着这些问题来阅读下面的文章refs 在函数组件的作用我们知道,函数组件是没有状态的。因此,我们想想获取函数组件,不能是在类组件中那么使用,那么,如果我们想获取函数组件内部的dom 或者react 元素呢? 此时,我们就需要使用ref转发了.使用方法ref转发 最关键的是在于转发, 必须需要使用 React.forwardRef() 这个内置的高阶组件,这个高阶组件返回一个新的组件,在调用原组件的时候,需要通过高阶组件包裹的新组件来实现函数组件使用ref 转发案例:import React, { Component } from 'react' interface IAP { msg: string, // 在使用ref 转发的时候,会默认传递两个参数,第一个是函数组件原有的props,第二个参数是ref // 如果使用ts 需要注意,第二个参数的的类型。要于下面需要使用ref的类型一致,如下,我们是在h1 元素中使用,那么类型就是 HTMLHeadingElement function A (props: IAP, ref: React.Ref<HTMLHeadingElement >){ // 这里拿到的ref 是一个空值,为啥呢? ref 都还没有完成绑定,但是在控制台中没有展开前是null, 展开对象会触发getter 方法,是会有值的。 console.log(ref); return ( <h1 ref={ref}>{props.msg}</h1> const NewA = React.forwardRef(A); export default class TestForwardRef extends Component { // 我们在接收使用的ref,定义也要是相同的类型。但是这里ts 好像不强制要求。我们自己可以人为来规定,避免类型不匹配 private getRef = React.createRef<HTMLHeadingElement>(); // 组件完成挂载就打印当前的结果 componentDidMount() { // 这里打印的是获取到的真实的ref 的值 console.log(this.getRef); render() { return ( <div> // 需要使用包装过后的组件,传递的参数是和原有组件一致的。 <NewA msg="我是组件A" ref={this.getRef}></NewA> </div> 结果:类组件使用ref 转发函数组件是在第二个参数中获取ref,那么类组件中怎么获取呢? 因为类组件是没有和函数组件那样传参的。我们仔细想一想,在类组件中我们是可以直接使用ref的,所以对于属性来讲,在转发的时候就不能使用ref 这个原来的属性名了。这里需要注意React.forwardRef里面可以传递一个函数,函数的参数如下:案列:import React, { Component } from 'react' interface IAP { // 其他参数 msg: string, // ref转发获取A组件的想要的dom forwardRef: React.Ref<HTMLHeadingElement> // 我们知道类组件中,传递参数是从 props 里面来进行传参的,所以这里就通过属性来传递 class A extends Component<IAP> { render() { console.log(this.props,'------'); return ( <h1 ref={this.props.forwardRef}>{this.props.msg}</h1> // 定义转发的参数类型 interface IForwardProp { // 其他参数 msg: string, // React.forwardRef 里面填写一个函数,函数有两个参数,第一个是默认类组件的props, 第二个参数的ref 转发的对象,这里需要注意 ref 的类型 const NewA = React.forwardRef((props:IForwardProp , ref: React.Ref<HTMLHeadingElement>) => { return (<A {...props} forwardRef={ref} />) export default class TestClassForwardRef extends Component { // 绑定需要使用的地方,注意类型需要一致 private getRef = React.createRef<HTMLHeadingElement>(); componentDidMount() { console.log(this.getRef); render() { return ( <div> // 使用方法 <NewA msg='我是A类组件' ref={this.getRef}></NewA> </div>
含义以及使用方法ref (reference)是引用,在 vue 中是用于或者真实的dom, 那么在react 中,ref 的作用是啥呢?场景:希望直接使用dom元素中的某个方法,或者希望直接使用自定义组件中的某个方法ref作用于内置的html组件ref作用于内置的html组件,得到的将是真实的dom对象使用string 方式import React, { Component } from 'react' export default class TestRef extends Component { // 组件挂载完成后打印this componentWillMount() { // 可以通过this.refs.属性名来进行获取 console.log(this); render() { return ( <div ref='test'> <h1>234234324</h1> </div> }注意:string 类型的已经过时了哦,不建议使用哦使用函数方式函数的调用时间:1.componentDidMount的时候会调用该函数 1.在componentDidMount事件中可以使用ref2.如果ref的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在componentDidUpdate之前 1.旧的函数被调用时,传递null 2.新的函数被调用时,传递对象3.如果ref所在的组件被卸载,会调用函数import React, { Component } from 'react' export default class TestRef extends Component { // 组件挂载完成后打印this componentWillMount() { // 这里的this.refs里面是没有任何东西的 console.log(this); render() { return ( <div ref={(el) => { // 这里可以获取到ref绑定dom 元素,前提是ref 是作用在react 内置组件的。 console.log(el) }}> <h1>234234324</h1> </div> }效果:注意:我上面直接在ref 后面绑定的函数不太建议使用哦,原因是每一次渲染(render)或者说是每一次 使用 this.setState({}) 都会创建一个匿名函数,并且进行执行。解决办法是,直接定义一个函数来进行绑定,就不会出现这个问题的,结果:使用react对象方式需要使用 React.createRef() 这个方式来定义一个对象样例:import React, { Component } from 'react' export default class TestRef extends Component { // 定义一个对象的属性,类型是react的对象 private getRef:React.RefObject<HTMLDivElement>; constructor(props:{}){ super(props); this.getRef = React.createRef(); // 组件挂载完成后打印this componentDidMount() { // 可以通过this.getRef.current 属性名来进行获取 console.log(this); render() { return ( <div ref={this.getRef}> <h1>234234324</h1> </div> 结果:从这个结果来看,我们发现,getRef 是一个普通对象,那么我们手动定义一个对象可以么?答案是可以的哦?总的来说,ref 需要使用的最好方式是 直接在属性中定义函数或者 属性来进行赋值,这样的效率最好ref作用于类组件,得到的将是类的实例我们知道 上面所说的 react 的内置组件 如 div, ul, li 等普通的html元素获取到的是真实的样例代码import React, { Component } from 'react' interface IProp<T> { msg: string, ref: React.RefObject<T> // 定义A组件 class A extends Component<IProp<A>> { render(){ return <h1>{this.props.msg}</h1> export default class TestRef extends Component { private getRef:React.RefObject<A> = React.createRef(); // 组件挂载完成后打印this componentDidMount() { // 可以通过this.getRef.current 属性名来进行获取 实列对象 console.log(this); render() { return ( <div> <A msg={'2r3242344'} ref={this.getRef}></A> </div> 结果:ref不能直接作用于函数组件函数是没有实例,没有声明周期,ref 不能直接作用于函数组件,如果需要,那么需要使用ref 转发总结 何时使用 Refs下面是几个适合使用 refs 的情况:管理焦点,文本选择或媒体播放。触发强制动画。集成第三方 DOM 库。避免使用 refs 来做任何可以通过声明式实现来完成的事情。
工欲善其事必先利其器,开发前端的过程中,一个好的编辑工具可以事倍功半。我认为比较好的有:vscode:免费开源,在开发vue和react 挺好的,如果vue 和 react 结合ts ,那么这个编辑器讲会是一个神器,爽的不得了。而且比较轻量级的。webstorm: 收费可破解,vue 和 react 不结合 ts 也挺好用的,本人在不使用ts 的时候就用这个,但是有一个致命的缺点,特别重。vscodeAuto Rename Tag 自动补全html 标签的代码Chinese (Simplified) Language Pack for Visual Studio Code 适用于 VS Code 的中文(简体)语言包Code Runner 安装好后,在代码重右键直接终端运行代码Code Spell Checker检查代码重单词拼写,好多同事在单词拼写中都会出现拼写错误,但是vscode 需要安装插件才会检查,webstorm 自带检查单词拼写问题ES7 React/Redux/GraphQL/React-Native snippets 对于es,react等代码中可以简写实现代码的通用模块ESLint: 对于代码检查时非常好用的,检查代码是否符合规范,用于团队协作,统一规范Live Server 在写html 和 css 中需要启动服务器可以使用这个插件,用于开启一个服务器来查看代码效果Markdown Preview Enhanced如果会写markdown 文件的,在vscode 可以使用这个插件,可以增强预览和在浏览器中打开等file-icons vscode 是不会自带图标的,安装这个插件可以使得文件图标不一样,可以轻松分辨各级各类文件和文件夹minapp 微信小程序标签、属性的智能补全(同时支持原生小程序、mpvue 和 wepy 框架,并提供 snippets)Mithril Emmet 输入关键字 使用 tab 来生成html 代码,遵循emmet 语法vue 和 Vetur 这两个是用于开发vue 代码必备的vscode 插件webstorm自带继承了好多插件,不列举哦
什么是声明文件在typescript中存在两种文件后缀名,一种是 .ts,另一种是.d.ts结尾的文件,我们主要的代码都是写在以.ts文件结尾的文件中。而.d.ts结尾的文件就是声明文件。声明文件的作用我们都知道,ts 是 js 的超集,ts 是一个静态的类型检查系统,ts 比 js 多的就是类型检查,而实现类型检查的关键就是 ts 中的声明文件。所以声明文件的作用是为js 提供类型检查而存在的。声明文件存放的位置放置到tsconfig.json配置中include:[]中包含的目录中放置到node_modules/@types文件夹中我们在node 中搭建ts的环境中,就需要安装 @types/node 这个声明文件。手动配置在 tsconfig.json中使用配置 typeRoots:[]里面配置,这里手动配置了就会失效 node_modules 和 include里面配置的。与JS代码所在目录相同,并且文件名也相同的文件。用ts代码书写的工程发布之后的格式。编写的ts 代码可以自动生成三个文件,js 文件 , .d.ts 文件, 和 .js.map 文件 ,但是后面两者都需要手动在 tsconfig.json中进行手动配置,生成编译生成js文件不需要配置的话会在当前ts 的目录下面生成js 文件,如果想向vue 打包生成outdir:'目录名称',生成.d.ts配置:"declaration": true,生成js.map需要配置:"sourceMap": true,编写声明文件声明文件的编写有两种方式,手动编写 自动生成,所有的声明文件都是给ts 认识的,改文件不参与函数的运行自动生成对于我们写的代码是ts 的代码,可以自动生成。方法工程是使用ts开发的,发布(编译)之后,是js文件,发布的是js文件。如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。配置tsconfig.json中的declaration:true即可手动编写全局声明:声明的文件放入的名字叫做gloab.d.ts上,不然会报错配置如下declare var 声明全局变量declare function 声明全局方法declare class 声明全局类declare enum 声明全局枚举类型declare namespace 声明全局对象(含有子属性)namespace表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问interface 和 type 声明全局类型这里不能使用 declare来进行声明,和 ts 里面的是一样的,所有声明文件都是给ts 来约束的,不会参与实际的代码运行。发布包还是两种包,一种是ts 开发的,另一种是给js开发声明文件当前工程使用ts开发编译完成后,将编译结果所在文件夹直接发布到npm上即可,手动开启那两个配置就好为其他第三方库开发的声明文件发布到@types/**中。1) 进入github的开源项目:https://github.com/DefinitelyTyped/DefinitelyTyped2) fork到自己的开源库中3) 从自己的开源库中克隆到本地4) 本地新建分支,在新分支中进行声明文件的开发在types目录中新建文件夹,在新的文件夹中开发声明文件5) push分支到你的开源库6) 到官方的开源库中,提交pull request7) 等待官方管理员审核(1天)审核通过之后,会将你的分支代码合并到主分支,然后发布到npm。之后,就可以通过命令npm install @types/你发布的库名
导言:我们都知道,ts 具有类型推导,并且可以很好的进行智能的类型推导。但是如果我们想要手动的来进行类型推导 —— 通过已知的类型来推断另一个类型,那么这个需要怎么做呢?关键字主要的关键字有以下几个: typeof,in, keyof 等关键字typeof关键字大家看到typeof, 肯定会说 js 中已经存在了哇,但是ts 中的typeof 有不一样的用法:这里ts 在 类型检查的时候报错,typeof 用在类型检查的位置。所以,在这里typeof的作用是:获取某个数据的类型, 上面的c 的类型是b的变量,这个b又是const定义的(const 和 let 定义的区别在于 const 定义常量, let 定义变量,详细)常量的值一般在声明的时候就要赋值,所以b 的类型不是string, 而是一个字面量:如何把上面代码修改的不报错呢?解决方法:方法一: 把const 修改成 let-方法二: 手动给const 声明类型:由上面的这个小例子,我们可以得出:TS中的typeof,书写的位置在类型约束的位置上。表示:获取某个数据的类型 但是typeof 作用于类的时候,确实一个类的构造函数。keyof 关键字作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型获取多个级联类型的交集如果没有交集的话,会类型推导出一个never类型获取多个级联类型的并集in 关键字该关键字往往和keyof联用,限制某个索引类型的取值范围。TS中预设的类型演算Partial<T> 将类型T中的成员变为可选样列原理分析/** * Make all properties in T optional type Partial<T> = { [P in keyof T]?: T[P]; // 使用泛型,把每一个属性都加上了可选的符号 };Required<T> 将类型T中的成员变为必填样列原理分析/** * Make all properties in T required type Required<T> = { [P in keyof T]-?: T[P]; // 这里的 - 号是去除条件 };Readonly<T> 将类型T中的成员变为只读样列源码分析/** * Make all properties in T readonly type Readonly<T> = { readonly [P in keyof T]: T[P]; // 就是在前面加了一个修饰符 };Exclude<T, U> 从T中剔除可以赋值给U的类型。样例源码分析/** * Exclude from T those types that are assignable to U type Exclude<T, U> = T extends U ? never : T; // 通过继承来实在U 包含 TExtract<T, U> 提取T中可以赋值给U的类型。样例源码分析/** * Extract from T those types that are assignable to U type Extract<T, U> = T extends U ? T : never; // 和上面的exclude 相反的NonNullable<T> 从T中剔除null和undefined。样例源码分析/** * Exclude null and undefined from T type NonNullable<T> = T extends null | undefined ? never : T;ReturnType<T> 获取函数返回值类型。样例-方式一:方式二:InstanceType<T> 获取构造函数类型的实例类型。
在做项目的时候,展示图片可能会用到nginx 的代理来进行展示,然而有些运维小哥哥喜欢展示技术,在展示图片的时候还需要前提传一个请求头,也就是账号和密码。在postman展示的情况如下:如果不使用这种方式,页面直接展示401没有权限。解决方法运维小哥哥一回头,搞得菜鸡的我就加班搞这个玩意,最后总结解决方法如下:postman 请求源码img标签请求携带请求头我们现在跨域思考一下,img标签是没有可以自定义请求头的。网上说的那些,个人感觉都不行,一个爬一个。我的解决办法分析: img src 是会自动发送请求的,所以我们可以认为src 里面的就是一个接口,只是该接口返回的是图片流数据,那么我们就可以在拿到图片地址的时候再来一次axios请求,在本次请求中加入请求头。代码如下:/** * 获取base64位的图片 * @param fileName {String} 文件名称 * @param caseId {String} 赔案号 * @returns {Promise<string>} export async function getImgURLOfBase64(fileName, caseId) { // 这里获取图片的地址,如果知道地址,直接绕过这部 const res = await getImgUrl({ // ...获取图片地址的参数 if (!res.data || res.data.length === 0) return ''; // 通过图片地址获取图片,从新获取图片 var config = { method: 'get', responseType: 'arraybuffer', url: '对应地址', headers: { // postman 中的请求头 // 重新获取请求,获取的是base64位的图片 return await axios(config).then(response => { return 'data:image/png;base64,' + btoa(new Uint8Array(response.data).reduce((data, byte) => data + String.fromCharCode(byte), '')); }把上面获取的base位的图片地址放在对应img src 里面就行了。
装饰器概念装饰器是面向对象的概念(在java中叫做注解,早c#中叫做特征,英文名字是decorator在angular大量使用,react中也会用到JS支持装饰器,目前处于建议征集的第二阶段,翻阅网上的大量文章,发现17年的时候就开始有装饰器了,也一直都在意见征集阶段,直到现在也是一样的。ts 中需要使用这个需要在 配置文件ts.config.json 中开启 experimentalDecorators: true就能使用装饰器了。 装饰器的作用看下面的例子中:class A { constructor( private prop1: string, private prop2: number, private prop3: boolean, private prop4: string, private prop5: number, private prop6: boolean, // ... 这里还有有100个属性 }我们现在要做一个事情,对上面的每一个属性进行验证。按照现在的ts 的方式,我们可以这么做:class A { constructor( private prop1: string, private prop2: number, private prop3: boolean, private prop4: string, private prop5: number, private prop6: boolean, // ... 这里还有有100个属性 *验证函数 public validate(){ if(!this.prop1 && this.prop1.length < 10){ // 验证prop1 if (this.prop2 > 0) { if (!this.prop3) { // ... }看上面的验证代码,我们可以发现一些不合理的地方,每一个属性都要验证,我们就要写一个if 来进行判断,100个属性就要写100遍,然后每一个if中的条件还有可能相同,就会导致我们写重复的代码,这是非常麻烦的。我们在写一个类的属性的时候,才是最清楚这个成员的条条框框,过了一会儿在写,可能都不记得。换一句话说是,类的成员与成员验证分开是不易于代码的维护的。解决的问题装饰器,能够带来额外的信息量,可以达到分离关注点的目的。信息书写位置的问题重复代码的问题上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。装饰器的作用:为某些属性、类、参数、方法提供元数据信息(metadata)元数据:描述数据的数据基于上面的这些因素,js 就提出了 装饰器的概念(现在依旧还没有正式发布),有了装饰器后,让前端的js 类的代码更像java 等后端语言了。但是我们要清楚的是,装饰器是js 中提出的,并不是ts 中的特殊语法。原理本质本质在JS中,装饰器是一个函数。(装饰器是要参与运行的)装饰器可以修饰:类成员(属性+方法)参数使用方法:使用装饰器@装饰器函数/** * 不为空判断 * @param target * @param prop function NotNull(target: any, prop: string) { // 判断静态属性 if (target.prototype && !target[prop]) { console.log(`${prop} 不能为空`) }else{ // 判断实例属性,这个需要使用工厂函数来实现动态创建类 if (!new (A as any)()[prop]) { console.log(`${prop} 不能为空`) class A { @NotNull private prop1: string = '' @NotNull private prop2: number = 1; @NotNull static prop3: boolean = false; }效果:原理上面的代码我们可以使用es5的代码来写出来进行分析:function A(){} A.prototype.prop1 = ''; __decorator([NotNull],A.prototype,'prop1'); A.prototype.prop2 = 1; // 这里传入的A.prototype稍后解释 __decorator([NotNull],A.prototype,'prop2'); // 静态属性 A.prop3 = false; // 这里传入的A 稍后解释 __decorator([NotNull],A,'prop3 '); *不为空的函数 function NotNull(target, prop){ if(!target[prop]){ console.log(`${prop} 这个属性不能为空`) *装饰者函数, 这个是自己根据网上的文档进行理解出来的。 function __decorator (decorators, target, prop){ for(var i = 0; i < decorators.length; i ++){ decorators[i](target, prop) }效果:通过上面的代码简写,我们可以得出以下结论(装饰器在ts中的前提)装饰器的执行时间是在 属性 定义好后就直接执行的装饰器就是一个函数,在ts 中会生成额外的代码装饰器可以做到面向切面编程(AOP),上面的代码不影响,是直接在定义就完成了验证。装饰器的使用方式类中使用装饰器类装饰器的本质是一个函数,该函数接收一个参数(必须),表示类本身(构造函数本身)使用装饰器@得到一个函数在TS中,如何约束一个变量为类Functionnew (参数)=>object装饰器函数的运行时间:在类定义后直接运行类装饰器可以具有的返回值:void:仅运行函数返回一个新的类:会将新的类替换掉装饰目标也可以在里面实现伪继承,但是不建议这么做(下面的列子是装饰器工厂)多个装饰器的情况:会按照后加入先调用的顺序进行调用。成员装饰器属性属性装饰器也是一个函数,该函数需要两个参数:1.如果是静态属性,则为类本身;如果是实例属性,则为类的原型;2.固定为一个字符串,表示属性名注意: 这里的测试需要ts 编译的结果是es6以上,不然A和A的原型都是undefined,得到的结果会是错误的。换一句话说,需要将 ts.config.json 的 target: 'es6' ,因为es5是没有类的,打印A和A的原型会报错哦方法方法装饰器也是一个函数,该函数需要三个参数:1.如果是静态方法,则为类本身;如果是实例方法,则为类的原型;2.固定为一个字符串,表示方法名3.属性描述对象成员也可以可以有多个装饰器修饰,和类装饰器使用的方式一样,也可以使用装饰器工厂参数中使用装饰器要求函数有三个参数:1.如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型2.方法名称或者undefined3.在参数列表中的索引使用第三方库元数据基础库 reflect-metadatanpm install reflect-metadata -S 这个库是用于保存元数据的初始值,这个是需要安装在生成环境的,因为装饰器是需要在代码运行时候起作用的。在 ts.config.json中加入配置emitDecoratorMetadata: true就会在编译结果中加入元数据。则TS在编译结果中,会将约束的类型,作为元数据加入到相应位置。这样一来,TS的类型检查(约束)将有机会在运行时进行。使用方式:看文档类的验证库class-validatornpm install class-validator --save 这个库是对类进行验证的,使用方式,看文档把普通对象转类 class-transformernpm install class-transformer --save 这个库是用于把一维的对象转成类,使用方式看文档上面的这两个库都需要依赖于 reflect-metadata (元数据的基础库)
什么是索引器我们都知道,ts 中 获取对象中的属性有好多种方式1.通过点的方式来进行获取2.通过属性表达式的方式进行获取3.通过获取对象的属性描述符进行获取我们获取属性的第二种方式,对象[值],使用成员表达式就叫做是索引器,索引器里面的内容不是ts 新增,这里只讨论如何ts 如何给索引器添加类型检查不用索引器存在的问题对象赋值绕过ts 检查解决办法在TS中,默认情况下,不对索引器(成员表达式)做严格的类型检查使用配置noImplicitAny:true开启对隐式any的检查。隐式any:TS根据实际情况推导出的any类型索引器特征在索引器中,键的类型可以是字符串,也可以是数字在类中,索引器书写的位置应该是所有成员之前class User{ [props:string]:any // 这里的any 可以是 联合类型等 name:string = 'cll' age: number = 9 const u = new User(); u['pid'] = '123'TS中索引器的作用在严格的检查下,可以实现为类动态增加成员可以实现动态的操作类成员在JS中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串。在TS中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配
类继承的作用继承可以描述类与类之间的关系,例如: 在斗地主的小游戏中,我们只看牌,牌里面有分4种花色的牌(除了大小王),♥,♠,♦,♣,我们可以知道♥的牌是牌,♠的牌是牌……,形如:在中文的描述种,什么是什么,例如 A 是 B,我们就可以理解成 A 是 B的子集(数学角度上)B 包含 A在程序编程思想中,如果A和B都是类,并且可以描述为A是B,则A和B形成继承关系:B是父类,A是子类B派生A,A继承自BB是A的基类,A是B的派生类如果A继承自B,则A中自动拥有B中的所有成员属性继承:获取父类牌的花色,这是一个父类的属性,字类可以直接拿到方法继承:假设牌都有一个方法叫做来摸我(瞎说的哈),这个方法应该是属于玩家类的行为,用于理解继承父类后,属性和方法也被同时继承。成员重写重写(override):子类中覆盖父类的成员,注意子类成员不能改变父类成员的类型无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。但是有一点,如果我们字类和父类的参数保持一致,但是如果父类的返回值类型是void,字类可以随便定义返回值类型。如下:原因:注意this关键字:在继承关系中,this的指向是动态——调用方法时,根据具体的调用者确定this指向;super关键字:在子类的方法中,可以使用super关键字读取父类成员(这个成员是指父类的方法,不能获取父类的属性)this 和 super 的区别( 在子类中)1.如果方法或者属性没有进行重写,那么this 和 super 是一样的。2.如果方法进行重写:。this 指向的是当前子类的实例,而super指向的是父类。super当作属性使用的时候, super 指向的是父类的原型,因此super无法拿到父类的实例属性,只能拿到父类的public和protected的方法,super 如果当方法使用的话,只能在字类的构造函数中并且是第一行使用,需要使用super()来实例化父类里面的属性,确保字类可以获取父类的成员。继承的类型匹配字类的对象,永远可以赋值给父类,(鸭子辩型法,或者子结构法)例如:面向对象中,这种现象,叫做里氏替换原则(是里氏这个人提出的原则,哈哈)如果需要判断一个数据的具体子类类型,可以使用instanceof类的单根性和传递性单根性:每个类最多只能拥有一个父类传递性:如果A是B的父类,并且B是C的父类,则,可以认为A也是C的父类
创建react 项目注意: 网上有一些生成react 的方法,但是也有一些是过时的。使用官方脚手架creact-react-app全局安装 creact-react-app 这个脚手架这个脚手架和 vue 的 vue-cli是一样的,都可以全局安装,命令: npm install -g creact-react-app , 安装好了后,直接使用 create-react-app <项目名> 就可以创建一个react 项目了直接使用npxnpx create-react-app <项目名>, 注意,npx 不是拼写错误,是 npm 在 5.2版本之后,推出了一个执行指定代码的一个指令,这种方式创建,感觉比较慢,想要快的同学可以使用vite, 接下来的文章中会有相关的描述.使用vite 进行搭建vite 到现在应该都不陌生,是一个比较新的构建工具。在开发速度的热加载中是快的惊人。语法: npm init @vitejs/app my-react-app --template react 更多指令,查看官方文档,可以把react 替换成 vue vue-ts 或者是下面图片中的任何一个搭建react + ts 的项目方式一: 使用官方的脚手架注意: npx create-react-app <项目名> --scripts-version=react-scripts-ts 网上大部分的这种方法已经过时npx create-react-app <项目名> -- typescript: 这个方法也没有了,只能创建js 的react正确的做法:npx create-react-app <项目名> --template typescript 跟多的react 结合 查看文档ps: 安装的时间比较久,想要快速,使用 vite效果: 这个是本人删除了原来的一点内容,准备拿来练手,然后觉得写篇文章,给自己的学习留下点足迹。方式二: 使用vite语法: npm init @vitejs/app <项目名称>--template react-ts效果:方式三: 使用webpack 每一个包独立安装待续。。。
上效果演示地址源码地址使用的技术栈前端vite: 一个刚出的构建工具,使用过后都说好,我是使用vite 来进行构建前端项目的。如何构建项目vue3: 这个也是刚出不久,许多生态也在慢慢的完善,关于vue2 升级到vue3的不同,可以查看element-plus:这是element团队推出支持vue3的ui组件库,我写文章这会儿还是测试版本,只是为了体验一下,顺便提点bug(感受一下不一样的bug),哈哈vue-router: 支持vue3的路由scss: css 的预编译处理器axios 向后台发送请求的库,既支持服务端,也支持客户端后台express:后台搭建服务的一个库,拥有良好的生态,例如:静态服务器,路由等log4js: 用于日志记录cors: 用于解决跨域的中间件问题总结element-plus form表单中 model 和 ref 的问题描述: 在form 表单中,model 和 ref 使用相同的值,并且这个值是在setup 函数中对外导出会引起表单的值的改变,导致表单的值不正确,并且无法正确和表单赋值解决办法:model 和 ref 使用不一样的变量名称,详情 查看vue3使用第三方插件需要使用对应vue版本问题描述: 在vue 的项目中,需要使用第三方的包来实现某个功能, 使用在vue 的SFC中,会报一个警告: [Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js",本项目使用 qrcode.vue这个包是可以直接支持vue3的,但是需要使用vue的别名解决办法:如果是vite搭建项目的话,在vite.config.js中直接指定vue的别名,如:'vue': 'vue/dist/vue.esm-bundler.js',详情,查看vue3使用js插件问题(js插件)描述: 这个js是指原生的js,不能是使用在vue2的插件,因为vue2插件里面的对应的vue实例不一样了。会报一个 插件打包方式的错误。解决办法:如果是vite搭建项目的话,在vite.config.js中加入以下配置: optimizeDeps: { include: ["qrcanvas"] },作用是:这样 vite 在执行 runOptimize 的时候中会使用 rollup 对 包含的 包重新编译,将编译成符合 esm 模块规范的新的包放入 node_modules 下的.vite_opt_cache中,然后配合 resolver 对 包含的包 的导入进行处理:使用编译后的包内容代替原来 qrcanvas 的包的内容,这样就解决了 vite 中不能使用 其他js包 的问题,这部分代码在 depOptimizer.ts 里。vue3在导入依赖问题本人使用的是 webstorm 编辑器,在导入模块的话,会自动导入,但是导入的模块会报错,原因是,vue3是基于现代的浏览器,导入的每一个模块都需要加入后缀名解决办法:每一个模块手动加入后缀名node中间件的加载顺序const express = require('express'); const path = require('path'); // 创建一个服务 const app = express(); const {defaultLogger} = require('./../config/logger') // 使用vue页面导航中间件,必须要放在前面,这个是针对的路由模式是history const history = require('connect-history-api-fallback'); app.use(history({ index: '/html/index.html', htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], // 不加这一行,后面有的请求会被拦截 rewrites: [ // 匹配到api开头的,继续向下传递 from: /^\/api/, to: function (context) { return context.parsedUrl.path; // 使用静态资源的中间件 app.use(express.static(path.resolve(__dirname, '../public'),{ index: ['html/index.html'], redirect: true, setHeaders: function (res, path, stat) { res.set("Access-Control-Allow-Origin","*") // 使用cors 跨域中间件 const cors = require('cors'); app.options('*', cors()) // 预检请求 app.use(cors({ "origin": "*", // 维护运行的的源头 "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", // 允许的请求方法名 "allowedHeaders": ['Authorization'], // 允许的请求头 "preflightContinue": true, // 解析完后,给下一个中间件 "optionsSuccessStatus": 200 // 响应的结果 // 使用urlencode 中间件来获取post contentType= application/x-www-form-urlencoded app.use(express.urlencoded({extended: true})) // 使用json 中间件来获取post contentTpe =application/json app.use(express.json()) // .... 业务逻辑的中间件 // 使用错误中间件 app.use(require('./../middleware/errorMiddleware')); app.listen(9011, () => { defaultLogger.info('服务启动了,正在监听9011端口,请请留意');
ts 类初探当大家看到typescript(ts) 中的类(class)时候,可能好多人都会想起面向对象,对的,面向对象是许多后台的一种编程思想,比如: 本人曾经接触的java, 里面就是用的是面向对象的思想。但是本文不讨论面向对象,值讨论ts 中 class 新增的语法,和一些使用方法以及注意事项。回顾es6中的类// 定义一个用户的类,里面有两个属性,名字和年龄 class User { constructor(name, age) { this.sex = "男"; this.id = Math.random().toString(32).substr(-6); this.name = name; this.age = age; getAge() { return this.age; * 静态方法,不能被实例化,就是说不可以通过new User().getNumber(), 这样调用时会报错的 * 调用的方式,可以使用 User.getNUmber(), 或者通过继承的方式进行调用 static getNumber() { return '1'; const u = new User(123, 12); console.log(u.getAge()); 123 console.log(User.getNumber()); 1问自己一个问题,上面的这种形式有没有存在些问题?缺少类型检查,上面user 里面的name 和 age 我们可以随便的传值。可能有的人会说,我们可以用set和get来进行限制。对的,如果能想到这个,确实学习的不错。但是考虑过一个问题吗? 我定义的一个类中,除了构造函数外(假设有100个需要类型检查的属性值),那里面岂不是全是get 和 set, 那这个和 java等后端的语言有啥区别?(这里不是对语言有偏见哈)哪些属性是Class 私有的,只读的,哪些是公开的呢? 有的同学说我们可以使用 es6 里面的 symbol(符号)来定义私有的,对的,但是个人感觉会有点小麻烦。🤭代码提示不是很友好(vscode有代码提示,往往都要开发者记忆类里面的关键词)等解决办法: ts 主角上场(ts 是静态的可以的类型检查系统,是没有说完全需要使用这个的哈) 传送门先看ts如何来解决上面的哪些问题:// require('./poker/index.ts') class User { // 在这里声明类的属性列表 readonly id: string // 定义只读属性 name: string; age?: number; // 定义可选属性 sex: "男" | "女" = "男" // 初始化赋值 constructor(name: string, age: number) { this.id = Math.random().toString(32).substr(-6); this.name = name; this.age = age; * 定义私有的函数 private getAge() { return this.age; }注意: 上面的代码,在代码的书写上,是比原来的es6的代码增多了。但是需要类型检查等功能,是会增加一层额外的代码。编译结果分析:ts Class 基本知识点属性使用属性列表来描述类中的属性:例如上面的:属性的初始化检查这个是检查,哪些属性可以有默认值,比如上面的sex,我们定义的类型是一个 类型别名,sex 那么是男,要么是女,我们完全可以使用默认值的,需要开启这个功能,在 tsconfig.json 中添加一个配置 strictPropertyInitialization:true属性初始化的位置1.在构造函数中,可以初始化属性的值2.给属性付给默认值在属性列表中,直接给属性付给默认值,这样也可以ok的。属性修饰符readonly: 只读的,写在属性,方法等的前面,标记改属性是只读,如果后序代码修改的化,ts 会给出类型检查报错public:公共的,写在属性,方法等的前面,但是这个修饰符一般可以不写,因为ts默认的都是所有的属性是公开的private: 私有的,写在属性,方法等的前面。标记改属性或者方法只能在类中使用,在其他地方使用,也会报错protected: 受保护的,书写的方式也是同样的,标记改属性或者方法是受保护的。在类里面、子类里面可以访问 ,在类外部没法访问。属性简写如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写, 例如:// 假设有一个类就是这样的 class User { name: string age: number constructor(name: string, age: number) { this.name = name; this.age = age; }可以简写 class User { constructor(public name: string, public age: number) { }效果:访问器作用:用于控制属性的读取和赋值也有两种方式: 一种是类似Java的形式,另一种是es 里面更新的java 形式的// class User { name: string age: number constructor(name: string, age: number) { this.name = name; this.age = age; setName(value: string) { // 这里可以对名字做业务处理 this.name = value; getName() { return this.name; const u = new User('cll', 12); u.name = 'cll123'; // 设置值 console.log(u.name) // 获取值效果:es7形式的// require('./poker/index.ts') // 假设有一个类就是这样对的 class User { _name: string age: number constructor(name: string, age: number) { this._name = name; this.age = age; set name(value: string) { // 这里可以对名字做业务处理 this._name = value; get name() { return this._name; const u = new User('cll', 12); u.name = 'cll123'; console.log(u.name) 效果:
ts(typescript) 模块化模块化标准ts 中的模块化,尽量统一使用 es6(es2015)的模块化标准前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext例如:// a.ts 文件 // 导出一个常量 export const a: number = 1; // 导出一个函数 export function b(a: number, b: number): number { return a + b; function defaultFunc() { console.log('this is default function') // 默认导出 export default defaultFunc; //index.ts 文件 // require('./poker/index.ts') import defaultFunc, { a, b } from "./a"; console.log(a, b(1, 2),defaultFunc())结果:编译结果对比ts 中可以通过使用 module 设置编译结果中使用的模块化标准,这个可以依照环境的不同来设置,如浏览器设置 es6, node 设置 commonjs等,这个是灵活处理的。ts 里面的代码使用的es6的导入和导出,编译的模块化是es6对比如下:结论: 上面的两张图中我们可以看到,ts里面的模块化使用es6,编译的结果也是es6,他们两者是没有区别的,但是要注意一个问题,导入模块的时候千万不能使用后缀名xxx.ts,因为编译后的结果是没有.ts后缀的文件的ts 里面的代码使用的es6的导入和导出,编译的模块化是commonjs对比如下结论:ts 的配置,编译的对象可以兼容很古老的语法,为了兼容旧版本,但是ts是遵从es标准的。如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exports的default属性;解决默认导入问题在index.ts中直接导入fs模块, 例如import fs from 'fs',但是结果确实报错的,如下:编译的结果如下(虽然说报错了, 但是ts默认是可以编译的,这个可以通过配置来进行配置):但是如何解决这个问题呢?方法一:按需导入 import { readFileSync } from 'fs' readFileSync('./')编译的结果如下:这个时可以正常使用的。方法二: 重命名的导入import * as fs from 'fs' fs.readFileSync('./')方法三:在ts.config.json中配置启用esModuleInterop 启用es模块化交互非es模块导出import fs from 'fs' fs.readFileSync('./')ts 中书写common js 的代码在ts 中,我们可以使用commonjs的语法,导出使用 module.export 或者 exports.xxx, 使用 require('xxx')来进行导入,但是这样的话就得不到 ts的类型检查了。如何既可以使用commonjs的语法,也要能进行类型检查呢?ts 新增了一个语法:导出:export = xxx 导入:import xxx = require("xxx")例如:// a.ts 文件 // 定义一个变量 const a: number = 1; // 定义一个函数 function b(a: number, b: number): number { return a + b; // 导出变量和函数 export= { a: a, b: b, // index.ts 文件 import obj = require('./a'); console.log(obj.a, obj.b(1, 2))运行结果如下:编译结果如下:a.ts 文件index.ts 文件ts的模块解析策略TS中,有两种模块解析策略classic:经典, 已经成为过去了node:node解析策略(唯一的变化,是将js替换为ts)。相对路径require("./xxx")。非相对模块require("xxx")可以使用配置文件来进行配置:在ts.config.json 中使用 moduleResolution 来进行模块解析策略的配置。本博客用到的ts的配置:配置名称含义module设置编译结果中使用的模块化标准moduleResolution设置解析模块的模式noImplicitUseStrict编译结果中不包含"use strict"removeComments编译结果移除注释noEmitOnError错误时不生成编译结果esModuleInterop启用es模块化交互非es模块导出详细配置参考
element-plus form ref 和 model值的关系感悟的记录elementui 是一个优秀的前端ui, 现在vue3 出来了,本人也想抓紧时间,赶紧给自己充电加油。现在的element-plus 是一个beta版本,我安装的element-plus 是 "^1.0.1-beta.24",遇到这个问题的时候是 "^1.0.1-beta.18" 这个版本,反正都是beta版本,只是解决了一些问题。我在element-plus 的 issue 里面看到了form表单中 ref 和 model 里面的值相同的时候,form表单时不能够修改的。有小伙伴回答到,当然不可以修改,去看vue的官方文档关于ref,所以个人觉得官方时不会修复这个问题了,在此记录一下,自己学习的心得。问题当使用element-plus的form表单的时候,ref 里面和 model 里面的绑定的值都时由setup函数返回的,页面里面的form表单无法添加值,并且控制台也报出警告,说form里面的表单的属性渲染通过,但是实例里面没有定义 Property xxx was accessed during render but is not defined on instance页面上打印from,里面却含有一些奇怪的值。setup 里面导出的是一个对象,页面上却发生了大变化? setup() { const formRef = ref({ text: '', margin: 0, size: 100, mainColor: '#000', subColor: '#fff' return { form: formRef, },如果看到该文章的同学比较急,可以直接往下面看解决办法,中间的原理可以跳过知识分解element-plus 中 form表单的model作用作用:配合表单验证目前el-form的model主要用表单验证的,也就是配合el-form的rules和el-form-item的prop来使用的。不信的话,你可以增加一个rules和prop(为了调用验证方法,也el-form也加一个ref属性,相当于id或者class选择器的意思),但是不写model,然后验证的话,会提示缺少model,导致无法验证成功。里面的逻辑大概是,在el-form-item上写一个prop,这个prop左手对应着数据源(即用model.prop找到对应的数据源),右手对应着验证规则(即用rules.prop找到对应的规则)。然后就快乐地验证去了。至于为什么不能将el-form的model+el-form-tem的prop这样的组合和表单中的v-model的用法合二为一,最直观的原因就是:作用于不同的标签,一种是针对表单的双向绑定,一种是针对el-form和el-form-item的验证(虽然这个验证的数据源最终就是表单那边双向绑定得来的);其次,你感觉一下,一边是利用双向绑定提供数据,另一边是拿到数据和规则进行验证,这两边没有很死板地捆绑在一起啊,类似于耦合度不高,未来自定义或者修改的话会方便很多源码中使用model 的地方,我们也可以看到,model就是用于表单验证的,如下图:vue中ref的作用vue2 :通过 vm.$refs来获取ref的指定的元素和dom实列vue3中:和vue2的用法一样如果vue3中不使用setup函数,里面的ref是一样的,但是如果使用setup的话,并且如果ref绑定的值是setup返回的话,就会产生后面的结果。作为模板使用的引用与任何其他引用的行为一样:它们是响应式的,可以传递给(或从)composition 函数中返回,我们可以很简单的理解,就是ref可以直接绑定 setup中返回的值,在虚拟DOM修补算法,如果VNode ref的键对应一个裁判在渲染上下文,VNode的相应元素或组件实例的值将分配给裁判。这是虚拟DOM挂载/补丁过程中执行,所以参模板后只会分配值初始渲染. 原文回到我们的代码中,我们可以发现,我们from对象在dom编译挂载的时候,已经被改变了,所以页面上才会出现model 和那些属性不存在的问题。import {ref,onMounted} from "vue"; export default { setup() { // 连接的地址,或者文件 const formRef = ref({ text: '', margin: 0, size: 100, mainColor: '#000', subColor: '#fff' onMounted(() => { // the DOM element will be assigned to the ref after initial render console.log(formRef.value,'===------') // 打印的是一个dom对象 console.log(formRef.value) // 这行代码会先打印,打印我们的form对象,原因是 js的事件循环机制 return { form: formRef, 解决办法如果不需要做表单验证,可以不需要使用model,可以不使用这个属性如果需要做表单验证,ref 和 model 绑定的值不一样,就不会出现这个问题了效果
vue3 使用第三方插件问题[Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js"上效果,解决问题问题描述:[Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js"解释一下上面的意思: 组件提供模板选项,但是在Vue的这个构建中不支持运行时编译,配置你的bundler别名 vue: vue/dist/vue.esm-bundler.js分析原因vue 的使用环境,分为两种环境,一种是开发,一种是生产,1.开发环境下:如果是vue2的话,需要依赖构建工具,如webpack, glup 等, 流程是 先使用对应的构建工具来进行构建编译生成一个一个的bundle, 然后才是运行。如果是vue3的话,有两种方式,一种是沿用vue2的开发模式,另一种是 使用 vite这个构建工具,流程是 基于现代浏览器的特点, 先查找相关的引用,然后在编译,在运行2.生成环境,都需要打包生成bundle 进行部署。基于vue 的不同环境需要使用的vue的代码也是不一样的,如下表:UMDCommonJSES Module (for bundlers)ES Module (for browsers)Fullvue.jsvue.common.jsvue.esm.jsvue.esm.browser.jsRuntime-onlyvue.runtime.jsvue.runtime.common.jsvue.runtime.esm.js-Full (production)vue.min.js--vue.esm.browser.min.jsRuntime-only (production)vue.runtime.min.js---这个表格来源是 vue-cli 里面介绍的,是指vue 在各个环境下面需要的不一样的版本,里面的每一个含义,麻烦查看官网,这里不复制黏贴。解决办法:vue3使用vite 构建: 项目根目录下面建立 vite.config.js配置别名, 详细配置 alias: { 'vue': 'vue/dist/vue.esm-bundler.js' // 定义vue的别名,如果使用其他的插件,可能会用到别名 },使用vue-cli 进行构建,项目根目录下面建立 vue.config.js 配置一个属性module.exports = { runtimeCompiler: true } // 确定是运行时候编译vue2 ,项目中建立对应的.config.jswebpackmodule.exports = { // ... resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js' }Rollupconst alias = require('rollup-plugin-alias') rollup({ // ... plugins: [ alias({ 'vue': require.resolve('vue/dist/vue.esm.js') })效果:不报警告了,插件也可以使用了
基本类型约束TS是一个可选的静态的类型系统,就是说,你在.ts文件中,用和不用ts的类型检查,都没有任何关系,因为ts是js的超集如何进行类型约束仅需要在 变量、函数的参数、函数的返回值位置加上:类型例如:let num: number = 5; // 类型检查数字类型 const str: string = 'str'; // 类型检查为字符串类型 const arr:number[] = [1,2,3] // 类型检查为数字数组,对于数组来说,ts会严格检查数组里面的每一个值是否符合,不符合直接提示报错 // 类型检查一个函数,已经函数里面的形参 function add (a: number, b: number): number { return a + b; }ts在很多场景中可以完成类型推导自动推断字符串类型let str = 'str';自动推断数字类型let age = 23;自动推断函数的返回值function add(a: number, b: number){ return a + b; }ts 可以在比较多的场景下,自动的推断出类型any: 表示任意类型,对该类型,ts不进行类型检查let a: any; a = 123; a = 'sty' a = {}上面的代码是不是有点像我们在js里面,定义一个变量,一会儿是数字,然后是字符串,再然后是对象……,最后这个变量是啥,我们开发者自己也不清楚了。小技巧,如何区分数字字符串和数字,关键看怎么读?如果按照数字的方式朗读,则为数字;否则,为字符串。源代码和编译结果的差异编译结果中没有类型约束信息.基本类型number:数字string:字符串boolean:布尔Array<类型>,类型:[]:数组 ,后面那一种写法是前一种的语法,一般推荐使用后面一种,因为在react中,<>代表是一个标签。object: 对象, 对象的检查有点弱,里面如果要严格检查里面的每一个属性,需要用到后面的接口或者类,或者是使用字面量的方式。null 和 undefinednull和undefined是所有其他类型的子类型,它们可以赋值给其他类型let a: string = undefined; a = '123'; a = null;上面的这种写法不报错,但是开发者普遍觉得都会比较乱,一会儿是字符串,一会儿是undefined,一会儿是null, 所以我们要避免这种情况。通过在tsconfig.json中添加strictNullChecks:true,可以获得更严格的空类型检查,null和undefined只能赋值给自身。如果在代码中确实需要使用,那么我们可以使用下面的其他常用类型的联合类型就ok.其他常用类型联合类型:多种类型任选其一配合类型保护进行判断类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 或者 if 可以触发类型保护,但是typeof 和 if 只能触发简单的类型保护。//回到上面的那个在项目中确实需要使用null和undefined, 我们可以,这么写,是不会报错的。 let a: string | undefined | null = undefined; a = '123'; a = null; void类型:通常用于约束函数的返回值,表示该函数没有任何返回// 形如下面这种直接在里面执行的函数,执行后没有返回值的,可以使用void来进行类型检查 function voidFunc(a: number, b: number):void{ console.log(a); console.log(b) 当然,如果不使用void, typescript会自动的智能的推断出该函数的类型。never类型:通常用于约束函数的返回值,表示该函数永远不可能结束// 形如,某些函数,永远执行都不能执行下一行的, function throwErr(msg: string):never{ throw Error(msg) console.log(msg) // 在真实的代码执行中,这一行代码永远不不可能被执行 }字面量类型:使用一个值进行约束// 这个约束特别强大,限定值的 // 例如: let sex :"男"|"女"; sex = "男" // 如果sex 为其他值,会直接报错的,因为sex就只有两个值可以选择元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定let arr:[number, string, null]; arr = [1, '2', null]; arr = [1,2,3,5]; // 这一行代码会报错 // 如下图: 数组里面的每一项都要和元组定义的类型一样,如果有任何一项不一样,那么类型检查将会通不过,直接报错。any类型: any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型,不到万不得已,尽量少用let a: string = undefined; a = '123'; a = null;枚举类型: 在拓展类型里面类型别名对已知的一些类型定义名称语法:type 类型名 = ... let user: { name: string age: number sex: "男" | "女" // 添加用户 function addUser(user: { name: string age: number sex: "男" | "女" // dosomething to add user // 上面的代码做类型检查,我们会发现user对应的类型检查,我们会重复书写,如果将来有一天,我们的user 对象的类型需要增加或者修改一个,那么将会比较麻烦。 // 此时我们就可以使用类型别名 type User = { name: string age: number sex: "男" | "女" let user: User; function addUser(user: User){ // do somethong to add user // 我们可以使用type 定义多个类型,来对对象进行详细的类型检查,或者其他的,都可以的函数的相关约束1.函数重载:在函数实现之前,对函数调用的多种情况进行声明语法:function name(参数); // 约束条件1,声明1 function name(参数); // 约束条件2,声明2 function name (参数){} // 函数实现// 场景,我们需要使用一个函数,如果 传入的两个参数 都是 number, 求乘积,两个字符串,求字符串拼接 function contactTwo(a: number | string, b: number | string): number | string { if (typeof a === 'number' && typeof b === 'number') { return a * b; else if(typeof a === 'string' && typeof b === 'string'){ return a + b; throw Error('a 和 b 的类型必须相同') 我们希望求乘积返回一个number,但是这个函数的结果返回值可以是number 或者 string,我们希望求乘积返回一个number,但是这个函数的结果返回值可以是number 或者 string,修改方式// 求乘积 function contactTwo(a:number, b:number) :number; // 求字符串拼接 function contactTwo(a:string, b:string) :string; // 函数实现 function contactTwo(a: number | string, b: number | string): number | string { if (typeof a === 'number' && typeof b === 'number') { return a * b; else if(typeof a === 'string' && typeof b === 'string'){ return a + b; throw Error('a 和 b 的类型必须相同') 实现的效果如下:需要返回的number,方便类型检查需要返回的string,方便类型检查2.可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。function addNum(a:number, b: number, c?:number, d?:number){ // ... 这里需要对 c 和 d 做判断, 不然类型检查会报错 return a + b + c + d; }如果我们对函数的参数给上默认值,ts 会自动的推断,该参数是可选的类型function addNum(a: number, b: number, c: number = 0, d: number = 0) { return a + b + c + d;