我们开发WEB代码的时候,经常回遇到各种高度的计算. 因为总是忘记几者之间得区别,每次都要现查,这次通过这篇文章彻底搞明白这几个长度的区别。
分类: JavaScript
Thymeleaf公共css,js提取及自有css,js导入
之前参考网上的各种方法,均为达到期望的效果,于是到Thymeleaf 官网逛了下,找到官网的例子来实现了:
1 2 |
以fragment方式分离公有css和js, 以replace+参数的方式传入每个页面单独的css和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 |
公有css(存放在templates/common/htmlHead.html中): <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head th:fragment="common_header(title,links)"> <!-- Common styles and scripts --> <title th:replace="${title}">The awesome application</title> <meta charset="utf-8"></meta> <meta http-equiv="X-UA-Compatible" content="IE=edge"></meta> <meta name="renderer" content="webkit" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta> <meta name="description" content=""></meta> <meta name="author" content=""></meta> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"></meta> <link rel="icon" href="/img/icon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/favicon.ico" /> <link th:href="@{/webjars/bootstrap/3.3.7/dist/css/bootstrap.css}" rel="stylesheet"> <link href="css/common/top_header.css" rel="stylesheet"> <link href="css/common/bottom_footer.css" rel="stylesheet"> <!--<link th:href="${myCss}" rel="stylesheet">--> <link th:href="@{/webjars/cropper/2.3.4/dist/cropper.css}" rel="stylesheet"> <link href="css/common/rspMsg.css" rel="stylesheet"> <link href="css/common/common.css" rel="stylesheet"> <!--/* Per-page placeholder for additional links */--> <th:block th:replace="${links}" /> </head> |
一般页面调用公有CSS,并使用自己的CSS:
1 2 3 4 5 6 7 |
<!DOCTYPE html> <html lang="zh-CN"> <head th:replace="common/htmlHead :: common_header(~{::title},~{::link})"> <title>设置</title> <link rel="stylesheet" href="css/index.css"> </head> </html> |
公有js的提取方式是一样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="zh-CN"> <div th:fragment="common_js(scripts)"> <script th:src="@{/webjars/vue/2.3.4/dist/vue.js}"></script> <script th:src="@{/webjars/vue-resource/1.3.1/dist/vue-resource.js}"></script> <script th:src="@{/webjars/jquery/3.2.1/dist/jquery.js}"></script> <script th:src="@{/webjars/tether/1.4.0/js/tether.js}"></script> <script th:src="@{/webjars/bootstrap/3.3.7/dist/js/bootstrap.js}"></script> <script th:src="@{/webjars/cropper/2.3.4/dist/cropper.js}"></script> <script src="js/common/top_header.js"></script> <script src="js/common/common.js"></script> <!--/* Per-page placeholder for additional js */--> <th:block th:replace="${scripts}" /> </div> </html> |
公有JS的使用包一层DIV即可:
1 2 3 4 5 6 7 8 9 10 11 |
...... <body> ...... <!--使用公有js--> <div th:replace="common/htmlJS::common_js(~{::script})"> <!--每个页面自己的js--> <script src="js/index.js"></script> </div> </body> </html> |
测试结果可用,具体解释请参考Thymeleaf官方文档
注意,上述的配置在引入的是,会多出来一个DIV标签,如果想去掉这个标签,可以使用如下方式声明以及引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="zh-CN"> <th:block th:fragment="common_js(scripts)"> <script th:src="@{/webjars/vue/2.3.4/dist/vue.js}"></script> <script th:src="@{/webjars/vue-resource/1.3.1/dist/vue-resource.js}"></script> <script th:src="@{/webjars/jquery/3.2.1/dist/jquery.js}"></script> <script th:src="@{/webjars/tether/1.4.0/js/tether.js}"></script> <script th:src="@{/webjars/bootstrap/3.3.7/dist/js/bootstrap.js}"></script> <script th:src="@{/webjars/cropper/2.3.4/dist/cropper.js}"></script> <script src="js/common/top_header.js"></script> <script src="js/common/common.js"></script> <!--/* Per-page placeholder for additional js */--> <th:block th:replace="${scripts}" /> </th:block> </html> |
公有JS的使用包一层th:block即可:
1 2 3 4 5 6 7 8 9 10 11 |
...... <body> ...... <!--使用公有js--> <th:block th:replace="common/htmlJS::common_js(~{::script})"> <!--每个页面自己的js--> <script src="js/index.js"></script> </th:block> </body> </html> |
另外如果使用的SpringBoot版本是1.5.4,默认的thymeleaf不是3.0版本,上面的测试需要thymeleaf 3.0版本才可以,需要修改下pom.xml文件,添加以下配置即可:
1 2 3 4 5 6 7 8 |
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <!-- set thymeleaf version --> <thymeleaf.version>3.0.0.RELEASE</thymeleaf.version> <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version> </properties> |
另外如果使用的SpringBoot版本是2.5.0,则不需要任何修改,直接可以生效。
参考链接
thymeleaf与vue结合使用时,vue如何取模板里的值
1 2 3 4 5 |
<li th:each="grade : ${grades}" th:v-bind:class="|{current: gradeId==${grade.id}}|"> <a th:title="${grade.name}" href="javascript:void(0)" th:id="${grade.id}" th:text="${grade.name}" th:@click="|getCourses(${grade.id},subjectId,1)|" >二年级</a></li> |
1 |
th:@click="|getCourses(${grade.id},subjectId,1)|" |
@click为VUE里绑定的点击事件,此时事件存在于thymeleaf的循环th:each下的元素,getCourses() 为vue里的方法属于js,但是需要取到模板里产生的值<年级id>
此时可以用th:v-on:"| |" 或者th:@click="| |" 简单来说就是将前端的方法当作字符串拼接起来,前面加th:就能解析${grade.id} 的值
1 |
th:v-bind:class="|{current: gradeId==${grade.id}}|" |
同理,绑定class用于样式也能如此
参考链接
Vue axios 发送 FormData 请求
一、简介
axios 默认是 Payload 格式数据请求,但有时候后端接收参数要求必须是 Form Data 格式的,所以我们就得进行转换。
Payload 和 Form Data 的主要设置是根据请求头的 Content-Type 的值来的:
Payload:
1 |
Content-Type: 'application/json; charset=utf-8' |
Form Data:
1 2 3 |
Content-Type: 'application/x-www-form-urlencoded' Content-Type: 'multipart/form-data' |
上面三种 Content-Type 值介绍
application/json 和 application/x-www-form-urlencoded 都是表单数据发送时的编码类型。
form 的 enctype 属性为编码方式,常用有两种:application/x-www-form-urlencoded 和multipart/form-data,默认为 application/x-www-form-urlencoded。
当 action 为 get 时候,浏览器用 x-www-form-urlencoded 的编码方式把 form 数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串 append 到 url 后面,用 ?分割,加载这个新的 url。
当 action 为 post 时候,浏览器把 form 数据封装到 http body 中,然后发送到 server。
如果没有 type=file 的控件,用默认的 application/x-www-form-urlencoded 就可以了。
但是如果有 type=file 的话,就要用到 multipart/form-data 了。浏览器会把整个表单以控件为单位分割,并为每个部分加上 Content-Disposition(form-data或者file)、Content-Type(默认为text/plain)、name(控件name) 等信息,并加上分割符 (boundary)。
二、发送 formdata 请求(下面有这几种方式格式化参的数据样本,用于参考比较,看需求选择方式)
方式一,自己封装一个格式化函数:
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 |
import axios from 'axios' ################################### 请求方式一,全局使用 // 创建 axios 实例 const service = axios.create({ baseURL: '', timeout: 20000, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) // 将请求数据转换成功 formdata 接收格式 service.defaults.transformRequest = (data) => { return stringify(data) } ################################### 请求方式二,局部使用 axios({ method: 'post', url: 'http://localhost:8080/dzm', data: { username: 'dzm', password: 'dzm123456' }, transformRequest: [ function (data) { // 将请求数据转换成功 formdata 接收格式 return stringify(data) } ], headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) ################################### 转换方法封装 // 将参数转换成功 formdata 接收格式 function stringify (data) { const formData = new FormData() for (const key in data) { // eslint-disable-next-line no-prototype-builtins if (data.hasOwnProperty(key)) { if (data[key]) { if (data[key].constructor === Array) { if (data[key][0]) { if (data[key][0].constructor === Object) { formData.append(key, JSON.stringify(data[key])) } else { data[key].forEach((item, index) => { formData.append(key + `[${index}]`, item) }) } } else { formData.append(key + '[]', '') } } else if (data[key].constructor === Object) { formData.append(key, JSON.stringify(data[key])) } else { formData.append(key, data[key]) } } else { if (data[key] === 0) { formData.append(key, 0) } else { formData.append(key, '') } } } } return formData } |
方式二,使用 qs 组件,但是 qs 格式化会过滤空数组数据:
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 |
import axios from 'axios' // qs 模块是安装 axios 模块的时候就有的,不用另行安装,通过 import 引入即可使用 import qs from 'qs' ################################### 请求方式一,全局使用 // 创建 axios 实例 const service = axios.create({ baseURL: '', timeout: 20000, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) // 将请求数据转换成功 formdata 接收格式,这一段选方式一那种拦截转换也可以。 service.interceptors.request.use(config => { const token = Vue.ls.get(ACCESS_TOKEN) if (token) { // 让每个请求携带自定义 token 请根据实际情况自行修改 config.headers['X-Token'] = token } // 将请求数据转换成功 formdata 接收格式 config.data = qs.stringify(config.data) return config }, err) ################################### 请求方式二,局部使用 axios({ method: 'post', url: 'http://localhost:8080/dzm', data: { username: 'dzm', password: 'dzm123456' }, transformRequest: [ function (data) { // 将请求数据转换成功 formdata 接收格式 return qs.stringify(data) } ], headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) |
方式三,数组会被转换成字符串(这种不是特殊情况一般不会使用上)
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 |
import axios from 'axios' ################################### 请求方式跟上面一样 axios({ method: 'post', url: 'http://localhost:8080/dzm', data: { username: 'dzm', password: 'dzm123456' }, transformRequest: [ function (data) { // 将请求数据转换成功 formdata 接收格式 return stringify(data) } ], headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) ################################### 转换方法封装 // 将参数转换成功 formdata 接收格式 function stringify (data) { let ret = '' for (const it in data) { ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' } ret = ret.substring(0, ret.lastIndexOf('&')) return ret } |
三、上面方式,参数格式化之后:
方式一 格式化出来的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 数组无值 id: 2086 intention: follower_id[]: concat_material[]: // 数组有值 id: 2086 intention: follower_id[0]: 351 follower_id[1]: 66 // 数组 json 为空会被转成正常的数组,有值会被转成字符串,所以服务器需要注意处理 concat_material: [{"fname":"视频订单.xls","key":"local/other/099f4be38fb8e69bb031cbc36ed283a6.xls"}] |
方式二 格式化出来的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 数组无值 id: 2086 intention: // 数组有值 id: 2086 intention: follower_id[0]: 351 follower_id[1]: 66 concat_material[0][fname]: 视频订单.xls concat_material[0][key]: local/other/099f4be38fb8e69bb031cbc36ed283a6.xls concat_material[1][fname]: 视频订单1.xls concat_material[1][key]: local/other/099f4be38fb8e69bb031cbc36ed283a8.xls |
方式三 格式化出来的数据:
1 2 3 4 5 6 7 8 9 10 11 |
// 数组无值 id: 743 intention: 2 follower_id: concat_material: // 数组有值 id: 2086 intention: follower_id: 66,351 concat_material: [object Object],[object Object] |
参考链接
解决Uncaught (in promise) Error: Navigation cancelled from “/...“ to “/...“ with a new navigation.
解决
1 |
Uncaught (in promise) Error: Navigation cancelled from “/Search#1608911018888” to “/Search#1608911019245” with a new navigation. |
这个错误是vue-router内部错误,没有进行catch处理,导致的编程式导航跳转问题,往同一地址跳转,或者在跳转的 mounted/activated 等函数中再次向其他地址跳转时会报错。
push和replace都会导致这个情况的发生
解决方法为在路由中进行如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import VueRouter from 'vue-router'; Vue.use(VueRouter); //解决编程式路由往同一地址跳转时会报错的情况 const rop = VueRouter.prototype.push; const ror = VueRouter.prototype.replace; //push VueRouter.prototype.push = function (location, onResolve, onReject) { if (onResolve || onReject) return rop.call(this, location, onResolve, onReject) return rop.call(this, location).catch(err => err) }; //replace VueRouter.prototype.replace = function (location, onResolve, onReject) { if (onResolve || onReject) return ror.call(this, location, onResolve, onReject) return ror.call(this, location).catch(err => err) }; .............................. new Vue({ el: '#q-app', router: router, }); |
参考链接
Tab切换以及缓存页面处理的几种方式
前言
相信tab切换对于大家来说都不算陌生,后台管理系统中多会用到。如果不知道的话,可以看一下浏览器上方的标签页切换,大概效果就是这样。
1.如何切换
-
使用动态组件,相信大家都能看懂(部分代码省略)
1234567891011121314151617//通过点击就可以实现两个组件来回切换<button @click="changeView">切换view</button><component :is="currentView"></component>import pageA from "@/views/pageA";import pageB from "@/views/pageB";computed: {currentView(){return this.viewList[this.index];}},methods: {changeView() {this.index=(++this.index)%2}}注:这个多用于单页下的几个子模块使用,一般切换比较多使用下面的路由
- 使用路由(这个就是配置路由的问题了,不作赘述)
2.动态生成tab
一般UI框架给我们的tab切换都像是上面的那种,需要自己写入几个tab页之类的配置。但是我们如果想要通过点击左边的目录来生成一个tab页并且可以随时关闭呢(如下图)?
只需要给路由一个点击事件,把你的路由地址保存到一个列表,渲染成另一个平铺的tab目录即可
假设你的布局是这样,左边的目录,上边的tab,有字的是页面
1 2 3 4 5 |
<menu> <menu-item v-for="(item,index) in menuList" :key="index" @click="addToTabList(item.path)"> <router-link :to="item.path">{{item.name}}</router-link> <menu-item> </menu> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<template> <menu class="left"/>//menu代码部分如上 <div class="right"> <tab-list> <tab-item v-for="(item,index) in tabList" :key="index"> <router-link :to="item.path">{{item.name}}</router-link> <icon class="delete" @click="deleteTab"></icon> </tab-item> </tab-list> <page-view> <router-view></router-view>//这里是页面展示 </page-view> </div> </template> |
以上代码并非实际代码,只提供一个大概的思路。至于
addToTabList
和deleteTab
怎么做就是数组方法的简单push
和splice
操作了。为了效果好看,我们可能还需要一些tab
的active
样式,这里不作演示。
3.缓存组件
仅仅是做tab切换,远远是不够的,毕竟大家想要tab页就是要来回切换操作,我们需要保存他在不同tab里操作的进度,比如说填写的表单信息,或者已经查询好的数据列表等。
那么我们要怎么缓存组件呢?
只需要用到vue中的keep-alive组件
3.1 keep-alive
<keep-alive>
是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive>
与<transition>
相似,只是一个抽象组件,它不会在DOM树中渲染(真实或者虚拟都不会),也不在父组件链中存在,比如:你永远在this.$parent
中找不到keep-alive
。
注:不能使用
keep-alive
来缓存固定组件,会无效
1 2 3 4 |
//无效 <keep-alive> <my-component></my-component> </keep-alive> |
3.2 使用
3.2.1 老版本vue 2.1
之前的使用
1 2 3 4 |
<keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view> |
需要在路由信息里面设置router
的元信息meta
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export default new Router({ routes: [ { path: '/a', name: 'A', component: A, meta: { keepAlive: false // 不需要缓存 } }, { path: '/b', name: 'B', component: B, meta: { keepAlive: true // 需要被缓存 } } ] }) |
3.2.2 比较新而且简单的用法
- 直接缓存所有组件/路由
1 2 3 4 5 6 |
<keep-alive> <router-view/> </keep-alive> <keep-alive> <component :is="view"></component> </keep-alive> |
- 使用
include
来处理需要缓存的组件/路由
include
有几种用法,可以是数组,字符串用标点隔开,也可以是正则,使用正则的时候需要使用v-bind
来绑定。
1 2 3 4 5 6 |
<keep-alive include="['a','b']">//缓存name为a,b的组件 <keep-alive include ="a,b">//缓存name为a,b的组件 <keep-alive :include="/^store/">//缓存name以store开头的组件 <router-view/>//可以为router-view <component :is="view"></component>//也可以是动态组件 </keep-alive> |
- 使用
exclude
来排除不需要缓存的路由
跟
include
正好相反,在exclude
里的组件不会被缓存。用法类似,不作赘述
3.2.3 一种比较奇怪的情况
当页面跳转方式有A->C
和B->C
两种,但是我们从A到C的时候,不需要缓存,从B到C的时候需要缓存。这时候就要用到路由的钩子结合老版本用法来实现了。
1 2 3 4 5 6 7 8 9 10 |
export default { data() { return {}; }, methods: {}, beforeRouteLeave(to, from, next) { to.meta.keepAlive = false; // 让下一页不缓存 next(); } }; |
1 2 3 4 5 6 7 8 9 10 11 |
export default { data() { return {}; }, methods: {}, beforeRouteLeave(to, from, next) { // 设置下一个路由的 meta to.meta.keepAlive = true; //下一页缓存 next(); } }; |
3.3 缓存组件的生命周期函数
缓存组件第一次
打开的时候,和普通组件一样,也需要执行created
, mounted
等函数。
但是在被再次激活
和被停用
时,这几个普通组件的生命周期函数都不会执行,会执行两个比较独特的生命周期函数。
- activated
这个会在缓存的组件重新激活时调用 - deactivated
这个会在缓存的组件停用时调用
参考链接
JavaScript正则表达式匹配成对出现的标记(平衡组-balanced group)
XRegExp.matchRecursive(str, left, right, [flags], [options])
Requires the XRegExp.matchRecursive addon, which is bundled in xregexp-all.js
.
Returns an array of match strings between outermost left and right delimiters, or an array of objects with detailed match parts and position data. An error is thrown if delimiters are unbalanced within the data.
Parameters: |
|
---|---|
Returns: |
|
Example
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 |
// Basic usage let str = '(t((e))s)t()(ing)'; XRegExp.matchRecursive(str, '\\(', '\\)', 'g'); // -> ['t((e))s', '', 'ing'] // Extended information mode with valueNames str = 'Here is <div> <div>an</div></div> example'; XRegExp.matchRecursive(str, '<div\\s*>', '</div>', 'gi', { valueNames: ['between', 'left', 'match', 'right'] }); /* -> [ {name: 'between', value: 'Here is ', start: 0, end: 8}, {name: 'left', value: '<div>', start: 8, end: 13}, {name: 'match', value: ' <div>an</div>', start: 13, end: 27}, {name: 'right', value: '</div>', start: 27, end: 33}, {name: 'between', value: ' example', start: 33, end: 41} ] */ // Omitting unneeded parts with null valueNames, and using escapeChar str = '...{1}.\\{{function(x,y){return {y:x}}}'; XRegExp.matchRecursive(str, '{', '}', 'g', { valueNames: ['literal', null, 'value', null], escapeChar: '\\' }); /* -> [ {name: 'literal', value: '...', start: 0, end: 3}, {name: 'value', value: '1', start: 4, end: 5}, {name: 'literal', value: '.\\{', start: 6, end: 9}, {name: 'value', value: 'function(x,y){return {y:x}}', start: 10, end: 37} ] */ // Sticky mode via flag y str = '<1><<<2>>><3>4<5>'; XRegExp.matchRecursive(str, '<', '>', 'gy'); // -> ['1', '<<2>>', '3'] |
参考链接
vue中router-view与父组件之间的通信
在项目当中,遇到一个问题。当父页面的某个属性变化时,需要router-view中的页面根据不同的值进行不同的操作。
仔细想一下,其实类似父子组件之间的传值。
实现过程如下:
- 父组件绑定属性和事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<template> <router-view v-on:test="testP" v-bind:msg="msg"></router-view> </template> <script type="text/javascript"> export default { data() { return { msg: "把我带给router-view吧!" } }, methods: { testP: function (data) { // 从router-view返回来的数据 console.log(data) // 打印出来就是 // 把我带给父组件吧!第一次! // 把我带给父组件吧!第二次! } } } </script> |
- router-view关联的属性和监听动作
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 |
<template> <div>{{msg}}</div> </template> <script type="text/javascript"> export default { props:['msg'], data() { return {} }, watch: { // 监听父组件的msg的变化 msg: function() { console.log(this.msg) // 打印出来就是 // 把我带给router-view吧! } }, mounted() { // this.init() }, methods: { init() { // 第一次向父组件传值 this.$emit("test", "把我带给父组件吧!第一次!") // 第二次向父组件传值 this.$emit("test", "把我带给父组件吧!第二次!") } } } |
注意:
1 |
<router-view v-on:test="testP" v-bind:msg="msg"></router-view> |
v-on 绑定的函数名 test
,尽量不要出现大写字母(驼峰命名)(比如 v-on:Aplus_clicked="testP"),否则在某些特殊使用方式的情况下,可能会出现无法触发的问题。
驼峰命名,可能会发生如下报错:
1 |
[Vue tip]: Event "aplus_clicked" is emitted in component <Anonymous> but the handler is registered for "Aplus_clicked". Note that HTML attributes are case-insensitive and you cannot use v-on to listen to camelCase events when using in-DOM templates. You should probably use "aplus_clicked" instead of "Aplus_clicked". |
参考链接
Evaluating JavaScript code via import()
The import()
operator lets us dynamically load ECMAScript modules. But they can also be used to evaluate JavaScript code (as Andrea Giammarchi recently pointed out to me), as an alternative to eval()
. This blog post explains how that works.
eval()
does not support export
and import
A significant limitation of eval()
is that it doesn’t support module syntax such as export
and import
.
If we use import()
instead of eval()
, we can actually evaluate module code, as we will see later in this blog post.
In the future, we may get Realms which are, roughly, a more powerful eval()
with support for modules.
Evaluating simple code via import()
Let’s start by evaluating a console.log()
via import()
:
1 2 3 4 5 6 7 8 |
const js = `console.log('Hello everyone!');`; const encodedJs = encodeURIComponent(js); const dataUri = 'data:text/javascript;charset=utf-8,' + encodedJs; import(dataUri); // Output: // 'Hello everyone!' |
What is going on here?
- First we create a so-called data URI. The protocol of this kind of URI is
data:
. The remainder of the URI encodes the full resource instead pointing to it. In this case, the data URI contains a complete ECMAScript module – whose content type istext/javascript
. - Then we dynamically import this module and therefore execute it.
Warning: This code only works in web browsers. On Node.js, import()
does not support data URIs.
Accessing an export of an evaluated module
The fulfillment value of the Promise returned by import()
is a module namespace object. That gives us access to the default export and the named exports of the module. In the following example, we access the default export:
1 2 3 4 5 6 7 8 9 10 11 |
const js = `export default 'Returned value'`; const dataUri = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(js); import(dataUri) .then((namespaceObject) => { assert.equal(namespaceObject.default, 'Returned value'); }) .catch((err) => { console.log(err.message); // "Importing a module script failed." // apply some logic, e.g. show a feedback for the user }); |
Creating data URIs via tagged templates
With an appropriate function esm
(whose implementation we’ll see later), we can rewrite the previous example and create the data URI via a tagged template:
1 2 3 4 5 6 7 8 9 |
const dataUri = esm`export default 'Returned value'`; import(dataUri) .then((namespaceObject) => { assert.equal(namespaceObject.default, 'Returned value'); }) .catch((err) => { console.log(err.message); // "Importing a module script failed." // apply some logic, e.g. show a feedback for the user }); |
The implementation of esm
looks as follows:
1 2 3 4 5 6 |
function esm(templateStrings, ...substitutions) { let js = templateStrings.raw[0]; for (let i=0; i<substitutions.length; i++) { js += substitutions[i] + templateStrings.raw[i+1]; } return 'da |
For the encoding, we have switched from charset=utf-8
to base64
. Compare:
- Source code:
'a' < 'b'
- Data URI 1:
data:text/javascript;charset=utf-8,'a'%20%3C%20'b'
- Data URI 2:
data:text/javascript;base64,J2EnIDwgJ2In
Each of the two ways of encoding has different pros and cons:
- Benefits of
charset=utf-8
(percent-encoding):- Much of the source code is still readable.
- Benefits of
base64
:- The URIs are usually shorter.
- Easier to nest because it doesn’t contain special characters such as apostrophes. We’ll see an example of nesting in the next section.
btoa()
is a global utility function that encodes a string via base 64. Caveats:
- It is not available on Node.js.
- It should only be used for characters whose Unicode code points range from 0 to 255.
Evaluating a module that imports another module
With tagged templates, we can nest data URIs and encode a module m2
that imports another module m1
:
1 2 3 4 5 6 7 8 |
const m1 = esm`export function f() { return 'Hello!' }`; const m2 = esm`import {f} from '${m1}'; export default f()+f();`; import(m2) .then(ns => assert.equal(ns.default, 'Hello!Hello!')) .catch((err) => { console.log(err.message); // "Importing a module script failed." // apply some logic, e.g. show a feedback for the user }); |
Further reading
- Wikipedia on Data URIs
- Section on
import()
in “JavaScript for impatient programmers” - Section on tagged templates in “JavaScript for impatient programmers”
参考链接
判断网页是通过PC端还是移动终端打开的
1 2 3 4 5 |
if((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) { window.location.href = ""; //手机 } else { window.location.href = ""; //电脑 } |
也可以执行其他操作:
1 2 3 4 5 |
if((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) { alert('您正在通过手机访问'); } else { alert("您在PC端访问"); } |
JS判断客户端是否是iOS或者Android手机移动端:
通过判断浏览器的 userAgent,用正则来判断手机是否是ios和Android客户端。代码如下:
1 2 3 4 5 6 7 |
<script type="text/javascript"> var u = navigator.userAgent; var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端 var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 alert('是否是Android:'+isAndroid); alert('是否是iOS:'+isiOS); </script> |
下面一个比较全面的浏览器检查函数,提供更多的检查内容,你可以检查是否是移动端(Mobile)、ipad、iphone、微信、QQ等。
第一种:
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 |
<script type="text/javascript"> //判断访问终端 var browser={ versions:function(){ var u = navigator.userAgent, app = navigator.appVersion; return { trident: u.indexOf('Trident') > -1, //IE内核 presto: u.indexOf('Presto') > -1, //opera内核 webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核 gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,//火狐内核 mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端 ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端 android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端 iPhone: u.indexOf('iPhone') > -1 , //是否为iPhone或者QQHD浏览器 iPad: u.indexOf('iPad') > -1, //是否iPad webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部 weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增) qq: u.match(/\sQQ/i) == " qq" //是否QQ }; }(), language:(navigator.browserLanguage || navigator.language).toLowerCase() } //使用方法: //判断是否IE内核 if(browser.versions.trident){ alert("is IE"); } //判断是否webKit内核 if(browser.versions.webKit){ alert("is webKit"); } //判断是否移动端 if(browser.versions.mobile||browser.versions.android||browser.versions.ios){ alert("移动端"); } </script> |
检测浏览器语言
1 2 3 4 5 |
currentLang = navigator.language; //判断除IE外其他浏览器使用语言 if(!currentLang){//判断IE浏览器使用语言 currentLang = navigator.browserLanguage; } alert(currentLang); |
第二种:
1 2 3 4 5 6 7 8 9 |
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) { //alert(navigator.userAgent); window.location.href ="iPhone.html"; } else if (/(Android)/i.test(navigator.userAgent)) { //alert(navigator.userAgent); window.location.href ="Android.html"; } else { window.location.href ="pc.html"; }; |
也可以通过这样来适配,然后直接转跳到移动端页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function mobile_device_detect(url){ var thisOS=navigator.platform; var os=new Array("iPhone","iPod","iPad","android","Nokia","SymbianOS","Symbian","Windows Phone","Phone","Linux armv71","MAUI","UNTRUSTED/1.0","Windows CE","BlackBerry","IEMobile"); for(var i=0;i<os.length;i++){ if(thisOS.match(os[i])){ window.location.href=url; } } if(navigator.platform.indexOf('iPad') != -1){ window.location.href=url; } var check = navigator.appVersion; if( check.match(/linux/i) ){ if(check.match(/mobile/i) || check.match(/X11/i)){ window.location.href=url; } } } |