工程化开发基本流程 vite
安装与使用安装vite
vite
依赖的node
版本 >=
12.0.0
打开终端,查看npm
版本:npm -v
npm
版本在6.x
及以下使用如下命令
aribnb-ssr
表示项目名称
template
表示预设模板(技术栈)
1 npm init vite@latest airbnb-ssr --template vue-ts
npm
版本在7
及7+
使用如下命令
1 npm init vite@latest airbnb-ssr -- --template vue-ts
切换到文件夹下,安装依赖
vite
构建基本配置由于是在linux
环境下,运行启动命令前,改下package.json
的配置
1 "dev" : "vite --host 0.0.0.0 --port 8081" ,
vite
和webapck
的不同:
没有vendor
文件,利用的是浏览器原生的esModule
配置@
别名 vite.config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig ({ plugins : [vue ()], resolve : { alias : { '@' : path.resolve (__dirname, './src' ), }, }, })
tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : [ "src/*" ] } } ,
目录结构解析 相比较于常规vue2
目录,index.html
抽离出来了,<scipt>
标签上多了type="module"
属性,使用了main.ts
作为项目的入口文件
安装less-loader
安装vue-router
安装 1 npm i vue-router@next -D
安装的是4.0.13
版本
引入 新建router
目录,并新建router.ts
router.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import home from '@/views/home/index.vue' ;import mine from '@/views/mine/index.vue' ;import {createRouter, createMemoryHistory, createWebHistory, createWebHashHistory} from 'vue-router' ;const routes = [ { path : '/home' , name : 'home' , component : home, meta : { title : '' , keepAlive : false } }, { path : '/mine' , name : 'mine' , component : mine, meta : { title : '' , keepAlive : false } } ] const router = createRouter ({ history : createWebHistory (), routes }) export default router
main.ts
中使用vue-router
插件
1 2 3 4 5 6 7 8 import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp (App )app.use (router) app.mount ('#app' )
使用 组件中使用router
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script setup lang="ts"> import {useRouter} from 'vue-router' const router = useRouter() </script> <template> <button @click="() => router.push({path: '/home'})">首页</button> <button @click="() => router.push({path: '/mine'})">个人中心</button> <router-view/> </template> <style> </style>
@/views/home/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup lang="ts"> import {useRouter,useRoute} from 'vue-router' const router = useRouter() const route = useRoute() console.log(route.params) </script> <template> 首页 <button @click="() => router.push({path: '/mine', query: {id: 1}})">跳转到个人中心</button> </template> <style> </style>
@/views/mine/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script setup lang="ts"> import {useRouter, useRoute} from 'vue-router' const router = useRouter() const route = useRoute() console.log(route.query) </script> <template> 个人中心 <button @click="() => router.push({name: 'home', params: {id: 2}})">回到首页</button> </template> <style> </style>
安装element-ui
安装 官网:https://element.eleme.io/#/zh-CN/component/installation
当前项目是基于vue-cli5
安装的vue2
的项目,提示core-js
版本过低
1 2 npm WARN deprecated core-js@2.6.12: core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.
但实际上package.json
中的core-js
版本是3.8.3
,可能是element-ui
中用到的版本吧,暂时跳过这一问题
引入 全量引入(不推荐) main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import ElementUI from 'element-ui' ; import 'element-ui/lib/theme-chalk/index.css' ; Vue .config .productionTip = false Vue .use (ElementUI ); new Vue ({ router, store, render : h => h (App ) }).$mount('#app' )
按需引入 借助 babel-plugin-component
,我们可以只引入需要的组件,以达到减小项目体积的目的。
main.js
1 npm install babel-plugin-component -D
然后,将 .babelrc
修改为:
1 2 3 4 5 6 7 8 9 10 11 12 { "presets" : [["es2015" , { "modules" : false }]], "plugins" : [ [ "component" , { "libraryName" : "element-ui" , "styleLibraryName" : "theme-chalk" } ] ] }
如果没有.babelrc
,应该有babel.config.js
原来的内容
1 2 3 4 5 module .exports = { presets : [ '@vue/cli-plugin-babel/preset' ] }
修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module .exports = { presets : [ '@vue/cli-plugin-babel/preset' , ["es2015" , { "modules" : false }] ], plugins : [ [ "component" , { "libraryName" : "element-ui" , "styleLibraryName" : "theme-chalk" } ] ] }
使用 全量引入使用 直接使用
看文档上哪个适合用的,复制然后改吧改吧
1 2 3 4 5 6 7 8 <el-row> <el-button>默认按钮</el-button> <el-button type="primary">主要按钮</el-button> <el-button type="success">成功按钮</el-button> <el-button type="info">信息按钮</el-button> <el-button type="warning">警告按钮</el-button> <el-button type="danger">危险按钮</el-button> </el-row>
按需引入使用 接下来,如果你只希望引入部分组件,那么需要在 main.js
中写入以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import { Button , Row } from 'element-ui' ; Vue .config .productionTip = false Vue .component (Button .name , Button ); Vue .component (Row .name , Row );new Vue ({ router, store, render : h => h (App ) }).$mount('#app' )
值得注意的是,组件库的配置和脚手架的配置要合并起来的,如果脚手架更新了,但组件库的配置写法没更新,就会有问题
很明显的一个例子就是,.babelrc
文件都换成了babel.config.js
如上写法,会提示如下错误:
按照提示,安装下对应的包
1 npm i babel-preset-es2015 -D
虽然安装成功了,但人家babel-preset-es2015
更新改名了
1 2 3 [root@VM-4-12-centos test2] npm WARN deprecated babel-preset-es2015@6.24.1: 🙌 Thanks for using Babel: we recommend using babel-preset-env now: please read https://babeljs.io/env to update!
再重新启动下项目,报如下错误
由于人家es2015
改名了,所以配置文件得修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module .exports = { presets : [ '@vue/cli-plugin-babel/preset' , ["@babel/preset-env" , { "modules" : false }] ], plugins : [ [ "component" , { "libraryName" : "element-ui" , "styleLibraryName" : "theme-chalk" } ] ] }
造成这些问题的原因在于,element
没有及时更新
如果不说怎么改,要真想解决这个问题,需要研究下babel-plugin-component
文档,看看目前针对最新的脚手架,它的配置是怎么样的
按需引入后的资源大小
安装element-plus
安装
引入 按需引入
先下载依赖,实现自动导入
1 npm i -D unplugin-vue-components unplugin-auto-import
在vite.config.ts
中新增配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import {ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig ({ plugins : [ vue (), AutoImport ({ resolvers : [ElementPlusResolver ()] }), Components ({ resolvers : [ElementPlusResolver ()] }) ], resolve : { alias : { '@' : path.resolve (__dirname, './src' ), }, }, })
在入口文件中导入,并挂载
1 2 3 4 5 6 7 8 9 10 11 import { createApp } from 'vue' import App from './App.vue' import router from './router' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp (App )app.use (router) app.use (ElementPlus ) app.mount ('#app' )
使用 home/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup lang="ts"> import {useRouter,useRoute} from 'vue-router' const router = useRouter() const route = useRoute() console.log(route.params) </script> <template> 首页 <button @click="() => router.push({path: '/mine', query: {id: 1}})">跳转到个人中心</button> <el-button type="primary">Primary</el-button> </template> <style> </style>
可以看到,后台只引入了button
相关的样式
前台:
eleme-plus
是主包
base.css
是基本样式文件
el-button
是按钮相关样式
可以看到,UI
库占用的体积很小,这个就是按需加载的好处
我们再引入一下消息提示的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <script setup lang="ts"> import {useRouter,useRoute} from 'vue-router' import { h } from 'vue' import { ElMessage } from 'element-plus' const router = useRouter() const route = useRoute() console.log(route.params) const openVn = () => { ElMessage({ message: h('p', null, [ h('span', null, 'Message can be '), h('i', { style: 'color: teal' }, 'VNode'), ]), }) } </script> <template> 首页 <button @click="() => router.push({path: '/mine', query: {id: 1}})">跳转到个人中心</button> <el-button type="primary">Primary</el-button> <el-button :plain="true" @click="openVn">VNode</el-button> </template> <style> </style>
相比较于button
,消息提示框的组件,我们全局可能都会用到,可以在全局引入
使用vm
实例上的config.globalProperty.XXX = YYY
来配置全局属性
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 import { createApp } from 'vue' import App from './App.vue' import router from './router' import ElementPlus , {ElMessage } from 'element-plus' import 'element-plus/dist/index.css' const app = createApp (App )app.config .globalProperties .$message = ElMessage app.use (router) app.use (ElementPlus ) app.mount ('#app' )
在组件中,使用vue
的上下文来使用$message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <script setup lang="ts"> import {useRouter,useRoute} from 'vue-router' import { h, getCurrentInstance} from 'vue' const router = useRouter() const route = useRoute() console.log(route.params) const {proxy}:any = getCurrentInstance() // vue3中没有this,获取上下文 const openVn = () => { proxy.$message({ message: h('p', null, [ h('span', null, 'Message can be '), h('i', { style: 'color: teal' }, 'VNode'), ]), }) } </script> <template> 首页 <button @click="() => router.push({path: '/mine', query: {id: 1}})">跳转到个人中心</button> <el-button type="primary">Primary</el-button> <el-button :plain="true" @click="openVn">VNode</el-button> </template> <style> </style>
吐槽 为啥还会有个element-plus
的chunk
文件,2M
多!!
安装eslint
安装
安装完之后,执行一下eslint
npx eslint --init
或npm init @eslint/config
备注:npm5.2
之后,内置了npx
包,可以直接运行了(找自己的node_modules
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [root@VM-4-12-centos airbnb-ssr] Need to install the following packages: @eslint/create-config Ok to proceed? (y) y ✔ How would you like to use ESLint? · style ✔ What type of modules does your project use? · esm ✔ Which framework does your project use? · vue ✔ Does your project use TypeScript? · No / Yes ✔ Where does your code run? · browser, node ✔ How would you like to define a style for your project? · guide ✔ Which style guide do you want to follow? · standard ✔ What format do you want your config file to be in ? · JavaScript Checking peerDependencies of eslint-config-standard@latest The config that you've selected requires the following dependencies: eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest eslint-config-standard@latest eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 @typescript-eslint/parser@latest ✔ Would you like to install them now? · No / Yes ✔ Which package manager do you want to use? · npm Installing eslint-plugin-vue@latest, @typescript-eslint/eslint-plugin@latest, eslint-config-standard@latest, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, @typescript-eslint/parser@latest added 96 packages, and audited 291 packages in 19s 82 packages are looking for funding run `npm fund` for details found 0 vulnerabilities Successfully created .eslintrc.js file in /root/hh_git/vue-project/airbnb-ssr
安装完毕后,会根据我们的选择,生成.eslintrc.js
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 module .exports = { env : { browser : true , es2021 : true , node : true }, extends : [ 'plugin:vue3/essential' , 'standard' ], parserOptions : { ecmaVersion : 'latest' , parser : '@typescript-eslint/parser' , sourceType : 'module' }, plugins : [ 'vue' , '@typescript-eslint' ], rules : { } }
使用 如果是在本地开发
我们导入了未使用的组件,则会出现错误提示
统一变量定义等
自定义规则:
1 2 3 4 rules : { 'space-before-function-paren' : 0 }
搭配vscode
插件:eslint
使用更佳
如果eslint
未生效,就要检查vscode
之前有没有安装过类似的格式化工具、自动美化的一些插件
如果是在linux
远程开发,暂时没办法
安装Saas
创建react项目 创建小程序项目 mock
流程最方便的 使用json-server
,见ajax
的文章
简单mock
以vue
为例,src
目录下新建mock
文件夹
文件夹结构如下:
1 2 3 mock |----data |----index.js
安装express
:npm i express -D
data
可以放一些假的返回数据,之后在index.js
中定义接口并返回数据
index.js
1 2 3 4 5 6 7 8 9 10 11 12 const express = require ('express' )const app = express ()const url = require ('url' )const getdata = require ('./data/spider/getdata.json' )app.get ('/get-data' ,(req, res ) => { res.send (getdata) }) app.listen (8081 , () => { console .log ('8081在运行' ) })
然后使用node
运行:node index.js
如果配置的端口是可以访问外网的话,输入ip+接口路径
,即可正常返回数据:
在后台接口还没有或不便于调试时,可以先用本地数据来开发,只要返回的数据格式一致即可
稍微复杂点的mock
mockjs
基本使用
创建src/mock
文件夹,新建相应的json
文件以及入口文件mockServer.js
1 2 3 4 import Mock from 'mockjs' import banner from './banner.json' Mock .mock ("/mock/banner" ,{code : 200 , message : "0k" , data : banner})
main.js
中引入
1 import '@/mock/mockServer.js'
对于mock
的请求地址,使用axios
时,要新建个axios
封装的文件(之前的是request.js
,现在复制一下改个名)
https://blog.csdn.net/weixin_46670795/article/details/109129987
1 2 3 4 5 6 7 cnpm install axios --save cnpm install mockjs --save-dev cnpm install json5 --save-dev
文件服务器搭建流程 https://www.jianshu.com/p/2b3b03368249
1 npm i express cors multer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const express = require ('express' )const app = express ()const cors = require ('cors' )const multer = require ('multer' )const fs = require ('fs' )const path = require ('path' )const upload = multer ({dest : './static/' })app.use (upload.any ()) app.use (express.json ()) app.use (express.urlencoded ({ extended : true })) app.use (cors ()) app.get ('/' , (req, res ) => { res.send ('ok' ) }) app.post ('/upload' , (req, res ) => { console .log (req.files ) var extname = path.extname (req.files [0 ].originalname ); var newPath = req.files [0 ].path + extname; fs.rename (req.files [0 ].path , newPath, function (err ){ if (err){ res.send ('上传失败' ) }else { res.send ('上传成功' ) } }) }) app.listen (80 , () => console .log ('server running!' ))
CSS解决方案 点击事件记录样式
背景,现在是hover状态下,加深背景色,希望点击时,也能加深背景色,以提示用户目前操作的是哪一个菜单栏
布局的依赖性问题
背景,如何避免改一个地方的样式,其他地方的样式还要跟着改
vue-cli
中引入公共样式
轮播图解决方案 使用swiper.js
main.js
中引入
1 import 'swiper/css/swiper.css'
https://www.swiper.com.cn/
在new swiper
实例之前,页面必须要有dom
结构,多以要watch
和$nextTick
配合使用
html
部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div class ="swiper-container" ref ="mySwiper" > <div class ="swiper-wrapper" > <div class ="swiper-slide" v-for ="(carousel, index) in bannerList" :key ="carousel.id" > <img :src ="carousel.imgUrl" /> </div > </div > <div class ="swiper-pagination" > </div > <div class ="swiper-button-prev" > </div > <div class ="swiper-button-next" > </div > </div >
js
部分
引入:
1 import Swiper from 'swiper'
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 watch : { bannerList : { handler (newValue, oldValue ) { this .$nextTick(() => { let mySwiper = new Swiper (this .$refs .mySwiper ,{ loop : true , pagination : { el : ".swiper-pagination" , clickable : true }, navigation : { nextEl : ".swiper-button-next" , prevEl : ".swiper-button-prev" } }) }) } } }
常见UI库解决方案 ElementUI el-popover
网络请求常见处理 取消请求发送 https://www.jianshu.com/p/22b49e6ad819
场景:搜索时,例如输入abc,尽管做了节流处理,但是在退格删除全部时,如果参数a的返回的数据量很大,即使一开始置空了,还是会回填参数a的结果,所以需要对输入间隔的时间戳进行判断,小于某个阈值时,取消axios请求
常见异常处理 axios异常处理
图片资源异常处理 参考链接:https://blog.csdn.net/mouday/article/details/107190403
利用onerror 事件处理img标签中的src图片加载失败
如果 img
标签中的src图片logo.png
加载失败,原来的图片位置会被error.png
替换
1 2 3 4 5 6 7 8 9 10 11 <img src ="logo.png" onerror ="handleImageError()" /> <script type ="text/javascript" > function handleImageError ( ) { console .log (event); var img = event.target ; img.src = "error.png" ; img.onerror = null ; } </script >
如果logo.png
不存在会触发 onerror事件,指定图片error.png
去替补,
如果替补图片error.png
还不存在,还会继续触发onerror事件,
需要使用img.onerror=null
取消事件处理
Vue处理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <template> <img v-bind="$attrs" v-on="$listeners" @error="handleError" /> </template> <script> /** * 有错误处理的图片 */ export default { name: "MoImage", data() { return { // 默认值 defaultImage: require("@/assets/image/image-default.png"), }; }, methods: { // 错误值处理 handleError(event) { event.target.src = this.defaultImage; // 控制不要一直跳动 event.target.onerror = null; }, }, }; </script>
交互方案 新增交互时,要考虑到
新增前用户可能停留的页面情况,在新增交互结束后,要能还原
交互周期
交互前
释放信号,通知用户新的交互已经开始
明确新增元素
明确新增元素和已有元素的依存关系
交互中
交互后
结束
提供结束交互入口
结束交互时,要考虑是否要清除当前的状态量
对于结束状态量的变化,要进行代码健壮性考虑
如:input输入框结束时进行清空值的操作,watch时对于oldValue为空的情况,就要进行处理
常见代码重构方案 条件嵌套重构方案 参考链接:https://juejin.cn/post/6844904006154715143
背景 日常开发经常会遇到复杂的条件判断, 一般做法就是用if
/else
, 或者优雅一点用switch
来实现多个条件的判断. 如果条件越来越多, 会导致代码越来越臃肿, 如何使用更优雅的方式来实现呢?
基本代码-if/elseif/else 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const clickHandler = (status ) => { if (status === 1 ) { sendLog ('processing' ) jumpTo ('IndexPage' ) } else if (status === 2 ) { sendLog ('fail' ) jumpTo ('FailPage' ) } else if (status === 3 ) { sendLog ('fail' ) jumpTo ('FailPage' ) } else if (status === 4 ) { sendLog ('success' ) jumpTo ('SuccessPage' ) } else if (status === 5 ) { sendLog ('cancel' ) jumpTo ('CancelPage' ) } else { sendLog ('other' ) jumpTo ('Index' ) } }
优化1-switch 通过以上代码, 可以看出该函数的作用是: 根据status
状态的不同, 发送日志和跳转到对应的页面.
大家可以轻易的使用switch
来进行重构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const clickHandler = (status ) => { switch (status) { case 1 : sendLog ('processing' ) jumpTo ('IndexPage' ) break case 2 : case 3 : sendLog ('fail' ) jumpTo ('FailPage' ) break case 4 : sendLog ('success' ) jumpTo ('SuccessPage' ) break case 5 : sendLog ('cancel' ) jumpTo ('CancelPage' ) break default : sendLog ('other' ) jumpTo ('Index' ) } }
这样看起来比if / else
清晰多了. 细心的你一定会发现case2
, case3
的逻辑是一样的,
优化2 在日常的代码开发中, 基本上大多数同学都是这样写. 这样写固然可以, 但也不太优雅. 有一观点是: 编程的本质, 数据结构
+ 算法
, 任何算法都包含两部分, Logic
+ Control
Logic部分就是真正意义上的算法
Control部分只是影响解决问题的效率.
如果我们能将 Logic
和 Control
部分有效地分开, 那么代码将会变得更加容易维护和改进.
比如, 我们试着用下面的办法去分离代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const actions = { '1' : ['processing' , 'IndexPage' ], '2' : ['fail' , 'FailPage' ], '3' : ['fail' , 'FailPage' ], '4' : ['success' , 'SuccessPage' ], '5' : ['cancel' , 'CancelPage' ], 'default' : ['other' , 'Index' ] } const clickHandler = (status ) => { let action = actions[status] || actions['default' ], LogName = action[0 ], pageName = action[1 ] sendLog (LogName ) jumpTo (pageName) }
这样的形式, 其实就是DSL(Domain Specific Language)
解析器.
DSL
的描述是一个Logic, 函数clickHandler
就是Control
部分, 代码大大简化。
由此可以总结出如下思想:
State Machine
DSL - Domain Specific Language
编程范式
面向对象: 委托, 桥接, 修饰, MVC…….
函数式编程: 修饰, 管道, 拼接
逻辑推导式编程
// TODO
文件导入导出 HTML转JSON 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 <template> <div class="box"> <input type="file" ref="fileItem"> <button id="btn" @click="submit">确定</button> </div> </template> <style scoped> body { margin: 0; } </style> <script> export default { data() { return { }; }, methods: { submit() { let file = this.$refs.fileItem.files.item(0) if (file.name.indexOf(".html") < 0) { // 不处理非html的文件类型 this.alertErr() return } // 获取文件里面的文本信息 file.text().then(res => { // 内容转成dom对象 let doms = this.parseToDOM(res); for (const dom of doms) { // 从dom对象中获取DL标签 if (dom.tagName == 'DL') { let result = this.textHandle(dom, null); this.exportRaw('data.json', JSON.stringify(result.children)) } } }) }, alertErr() { alert("请不要上传非浏览器书签文件") }, /** * 把String转为DOM对象 * @param str * @returns {NodeListOf<ChildNode>} */ parseToDOM(str) { let div = document.createElement("div"); if (typeof str == "string") { div.innerHTML = str; } return div.childNodes; }, /** * * @param dl * @param temp * @returns {*} */ textHandle(dl, temp) { // 先获取DL 下面的DT let dts = this.getDts(dl); if (dts.length > 0) { // 判断DT下面是否有DL标签 for (var i in dts) { let dt = dts[i], hdl = this.getTag(dt, "DL"); if (hdl != null) { let h = this.getTag(dt, "H3"); let returns = this.textHandle(hdl, {name: h.textContent, children: [], web: []}) if (temp == null) { temp = returns; } else { temp.children.push(returns); } } else { var a = this.getTag(dt, "A"); temp.web.push({ url: a.href, title: a.textContent, desc: a.textContent, logo: a.getAttribute("ICON") }) } } } return temp; }, /** * 获取DL下面的DT标签 * @param dl * @returns {[]} */ getDts(dl) { let dlcs = dl.children, arr = []; if (dlcs.length < 1) { return arr; } for (let dlc of dlcs) { if ((dlc.tagName.toUpperCase()) == 'DT') { arr.push(dlc) } } return arr; }, /** * 获取dt下面的标签 * * @param dl * @return */ getTag(dt, tagname) { let dtcs = dt.children, obj = null; if (dtcs.length < 1) { return obj } for (let dtc of dtcs) { if ((dtc.tagName.toUpperCase()) == tagname) { obj = dtc; break; } } return obj; }, /** * 导出为文件 */ exportRaw(name, data) { var urlObject = window.URL || window.webkitURL || window; var export_blob = new Blob([data]); var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a") save_link.href = urlObject.createObjectURL(export_blob); save_link.download = name; var ev = document.createEvent("MouseEvents"); ev.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); save_link.dispatchEvent(ev); } } }; </script>
文件上传 可拖拽方案 sortabeljs http://www.sortablejs.com/index.html
鼠标拖拽调整div大小 Javascript基本原理
根据鼠标位置改变鼠标样式
当鼠标在div的边缘和四个角时显示不同的样式,通过cursor修改
当鼠标在div的边缘和四个角按下时记录具体坐标点位置, 并开始根据鼠标的移动修改div的尺寸
鼠标松开时结束尺寸修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > <style > body , html { width : 100% ; height : 100% ; margin : 0 ; } #container { width : 200px ; height : 200px ; padding : 15px ; border : #00cdcd 2px solid; box-sizing : border-box; } .item { cursor : default; width : 100% ; height : 100% ; background : #757575 ; } </style > </head > <body id ="body" > <div id ="container" > <div class ="item" > </div > </div > <script > let c = document .getElementById ('container' ) document .getElementById ('body' ).addEventListener ('mousemove' , move) c.addEventListener ('mousedown' , down) document .getElementById ('body' ).addEventListener ('mouseup' , up) let resizeable = false let clientX, clientY let minW = 8 , minH = 8 let direc = '' function up ( ) { resizeable = false } function down (e ) { let d = getDirection (e) if (d !== '' ) { resizeable = true direc = d clientX = e.clientX clientY = e.clientY } } function move (e ) { let d = getDirection (e) let cursor if (d === '' ) cursor = 'default' ; else cursor = d + '-resize' ; c.style .cursor = cursor; if (resizeable) { if (direc.indexOf ('e' ) !== -1 ) { c.style .width = Math .max (minW, c.offsetWidth + (e.clientX - clientX)) + 'px' clientX = e.clientX } if (direc.indexOf ('n' ) !== -1 ) { c.style .height = Math .max (minH, c.offsetHeight + (clientY - e.clientY )) + 'px' clientY = e.clientY } if (direc.indexOf ('s' ) !== -1 ) { c.style .height = Math .max (minH, c.offsetHeight + (e.clientY - clientY)) + 'px' clientY = e.clientY } if (direc.indexOf ('w' ) !== -1 ) { c.style .width = Math .max (minW, c.offsetWidth + (clientX - e.clientX )) + 'px' clientX = e.clientX } } } function getDirection (ev ) { let xP, yP, offset, dir; dir = '' ; xP = ev.offsetX ; yP = ev.offsetY ; offset = 10 ; if (yP < offset) dir += 'n' ; else if (yP > c.offsetHeight - offset) dir += 's' ; if (xP < offset) dir += 'w' ; else if (xP > c.offsetWidth - offset) dir += 'e' ; return dir; } </script > </body > </html >
Vue实现 这个是自己的思路,很明显有问题,请看下一节
会有卡顿,在vue
中还是用原生js
实现:
vue @mousemove实现拖动,鼠标移动过快拖动卡顿_木荣小逗比的博客-CSDN博客_@mousemove
卡顿的原因,在于事件绑定的元素有没有对上,上述的move
事件,在vue
中的实现,如果能拿到container
的$event
就不会出现卡顿了
如下代码,向右拉时,会卡顿,向左拉,由于事件冒泡到了子元素,所以向左拉是正常的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> div#body { width: 100vw; height: 100vh; margin: 0; } #container { width: 200px; height: 200px; padding: 15px; border: #00cdcd 2px solid; box-sizing: border-box; } .item { cursor: default; width: 100%; height: 100%; background: #757575; } body { margin: 0; /* padding: 100px; */ } </style> </head> <body> <div id="app"> <div id="body" @mouseup="up()" @mousemove.capture="move($event)" > <div id="container" @mousedown.self="down($event)"> <div class="item"></div> </div> </div> </div> <script src="./js/vue.js"></script> <script> const app = new Vue({ el: "#app", data() { return { resizeable: false, clientX: 0, // 鼠标像素点到浏览器边缘的距离 clientY: 0, minW: 8, minH: 8, direc: '', body: null, container: null, } }, created() { this.$nextTick(() => { }) }, methods: { getDirection(ev) { let xP, yP, offset, dir; dir = ''; xP = ev.offsetX; yP = ev.offsetY; offset = 10; if (yP < offset) dir += 'n'; else if (yP > ev.target.offsetHeight - offset) dir += 's'; if (xP < offset) dir += 'w'; else if (xP > ev.target.offsetWidth - offset) dir += 'e'; // console.log('方向为:',dir) return dir; }, down($event) { let d = this.getDirection($event) // console.log($event.offsetX, $event.offsetY) // console.log($event.target.offsetWidth, $event.target.offsetHeight) // console.log($event.clientX, $event.clientY) // 当位置为四个边和四个角时才开启尺寸修改 if (d !== '') { this.resizeable = true this.direc = d this.clientX = $event.clientX this.clientY = $event.clientY } }, up() { this.resizeable = false }, move($event) { let d = this.getDirection($event) console.log(d) let cursor if (d === '') cursor = 'default'; else cursor = d + '-resize'; // 修改鼠标显示效果 $event.target.style.cursor = cursor; // 当开启尺寸修改时,鼠标移动会修改div尺寸 if (this.resizeable) { // 鼠标按下的位置在右边,修改宽度 if (this.direc.indexOf('e') !== -1) { $event.target.style.width = Math.max(this.minW, $event.target.offsetWidth + ($event.clientX - this.clientX)) + 'px' this.clientX = $event.clientX } // 鼠标按下的位置在上部,修改高度 if (this.direc.indexOf('n') !== -1) { $event.target.style.height = Math.max(this.minH, $event.target.offsetHeight + (this.clientY - $event.clientY)) + 'px' this.clientY = $event.clientY } // 鼠标按下的位置在底部,修改高度 if (this.direc.indexOf('s') !== -1) { $event.target.style.height = Math.max(this.minH, $event.target.offsetHeight + ($event.clientY - this.clientY)) + 'px' this.clientY = $event.clientY } // 鼠标按下的位置在左边,修改宽度 if (this.direc.indexOf('w') !== -1) { $event.target.style.width = Math.max(this.minW, $event.target.offsetWidth + (this.clientX - $event.clientX)) + 'px' this.clientX = $event.clientX } } } } }) </script> </body> </html>
向右拉会修改外层元素的width:
move
方法需要绑定在外层元素,因为要监听整个move
过程,但event
对象希望拿到子元素的
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 getEvent (e ) { console .log (e) }
这种方法行不通,根本还是在于事件冒泡的机制
三区域拖动 需求效果:
原理:拖动效果的实现基本都是dom操作来实现的,通过拖动分隔线,计算分隔线与浏览器边框的距离(left),来实现拖动之后的不同宽度的计算;当拖动分隔线1时,计算元素框left和mid;当拖动分隔线2时,计算元素框mid和right;同时设置元素框最小值以防止元素框拖没了(其实是被遮住了)。使用SetCapture() 和 ReleaseCapture()的函数功能指定窗口里设置鼠标捕获。
在vuejs中使用,methods设置方法,mounted钩子挂载:
html部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <div > <ul class ="box" ref ="box" > <li class ="left" ref ="left" > 西瓜</li > <li class ="resize" ref ="resize" > </li > <li class ="mid" ref ="mid" > 备注2</li > <li class ="resize2" ref ="resize2" > </li > <li class ="right" ref ="right" > test</li > </ul > <ul class ="box" ref ="box" > <li class ="left" ref ="left" > 芒果</li > <li class ="resize" ref ="resize" > </li > <li class ="mid" ref ="mid" > 备注</li > <li class ="resize2" ref ="resize2" > </li > <li class ="right" ref ="right" > test</li > </ul > </div > </template >
js部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 <script> export default { mounted () { this .dragControllerDiv (); }, methods : { dragControllerDiv : function ( ) { var resize = document .getElementsByClassName ('resize' ); var resize2 = document .getElementsByClassName ('resize2' ); var left = document .getElementsByClassName ('left' ); var right = document .getElementsByClassName ('right' ); var mid = document .getElementsByClassName ('mid' ); var box = document .getElementsByClassName ('box' ); for (let i = 0 ; i < resize.length ; i++) { resize[i].onmousedown = function (e ) { var startX = e.clientX ; resize[i].left = resize[i].offsetLeft ; document .onmousemove = function (e ) { var endX = e.clientX ; var rightW = right[i].offsetWidth ; var moveLen = resize[i].left + (endX - startX); var maxT = box[i].clientWidth - resize[i].offsetWidth ; if (moveLen < 150 ) moveLen = 150 ; if (moveLen > maxT - rightW - 150 ) moveLen = maxT - rightW - 150 ; resize[i].style .left = moveLen; for (let j = 0 ; j < left.length ; j++) { left[j].style .width = moveLen + 'px' ; mid[j].style .width = (box[i].clientWidth - moveLen - rightW - 10 ) + 'px' ; } } document .onmouseup = function (evt ) { document .onmousemove = null ; document .onmouseup = null ; resize[i].releaseCapture && resize[i].releaseCapture (); } resize[i].setCapture && resize[i].setCapture (); return false ; } } for (let i = 0 ; i < resize2.length ; i++) { resize2[i].onmousedown = function (e ) { var startX = e.clientX ; resize2[i].left = resize2[i].offsetLeft ; document .onmousemove = function (e ) { var endX = e.clientX ; var leftW = left[i].offsetWidth ; var moveLen = resize2[i].left + (endX - startX) - leftW; var maxT = box[i].clientWidth - resize2[i].offsetWidth - 5 ; if (moveLen < 150 ) moveLen = 150 ; if (moveLen > maxT - leftW - 150 ) moveLen = maxT - leftW - 150 ; resize2[i].style .left = moveLen; for (let j = 0 ; j < right.length ; j++) { mid[j].style .width = moveLen + 'px' ; right[j].style .width = (box[i].clientWidth - moveLen - leftW - 10 ) + 'px' ; } } document .onmouseup = function (evt ) { document .onmousemove = null ; document .onmouseup = null ; resize2[i].releaseCapture && resize2[i].releaseCapture (); } resize2[i].setCapture && resize2[i].setCapture (); return false ; } } } } } </script>
style部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <style scoped> ul ,li { list-style : none; display : block; margin :0 ; padding :0 ; } .box { width :800px ; height :32px ; overflow :hidden; } .left { width :calc (30% - 10px ); height :100% ; background :skyblue; float :left ; } .resize { width :5px ; height :100% ; cursor : w-resize; float :left ; } .resize2 { width :5px ; height :100% ; cursor : w-resize; float :left ; } .right { float :left ; width :35% ; height :100% ; background :tomato; } .mid { float :left ; width :35% ; height :100% ; background :#f00 ; } </style>
相关链接:
vue项目拖动实现修改左右宽度 - SegmentFault 思否
双区域拖动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style scoped> ul, li { list-style: none; display: block; margin: 0; padding: 0; } .box { width: 100%; height: 32px; overflow: hidden; } .left { width: calc(30% - 5px); height: 100%; background: skyblue; float: left; } .resize { width: 5px; height: 100%; cursor: w-resize; float: left; } .right { float: left; width: 1%; height: 100%; background: tomato; } .mid { float: left; width: 70%; height: 100%; background: #f00; } </style> </head> <body> <div id="app"> <ul class="box" ref="box"> <li class="left" ref="left">西瓜</li> <li class="resize" ref="resize"></li> <li class="mid" ref="mid">备注2</li> </ul> </div> <script src="./js/vue.js"></script> <script> new Vue({ el: "#app", mounted() { this.dragControllerDiv(); }, methods: { dragControllerDiv: function() { var resize = document.getElementsByClassName('resize'); var left = document.getElementsByClassName('left'); var mid = document.getElementsByClassName('mid'); var box = document.getElementsByClassName('box'); for (let i = 0; i < resize.length; i++) { resize[i].onmousedown = function(e) { var startX = e.clientX; // 获取分隔线,距离浏览器左边的距离 resize[i].left = resize[i].offsetLeft; // console.log(resize[i].offsetLeft) document.onmousemove = function(e) { var endX = e.clientX; var moveLen = resize[i].left + (endX - startX); var maxT = box[i].clientWidth - resize[i].offsetWidth - 500; if (moveLen < 150) moveLen = 150; if (moveLen > maxT - 150) moveLen = maxT - 150; resize[i].style.left = moveLen; for (let j = 0; j < left.length; j++) { left[j].style.width = moveLen + 'px'; mid[j].style.width = (box[i].clientWidth - moveLen - 10) + 'px'; } } document.onmouseup = function(evt) { document.onmousemove = null; document.onmouseup = null; resize[i].releaseCapture && resize[i].releaseCapture(); } resize[i].setCapture && resize[i].setCapture(); return false; } } } } }) </script> </body> </html>
历史记录方案 之前写过一遍了,同步的时候没注意丢掉了
导航历史记录 将localStorage
的读取,结合vuex
,利用数据代理封装成响应式
使用v-if/v-else
来设计好历史记录
和搜集结果
之间的结构,并处理好状态变更的交互
添加存储历史记录的逻辑,并控制好存储数量、去重操作、新浏览的记录更新至第一位,最后提交将序列化好的数据格式提交mutations
异常结果的考虑
未搜到结果的提示,不应该被记录下来
添加删除历史记录UI
界面及逻辑