lulu_up

lulu_up 查看完整档案

上海编辑辽宁工程技术大学  |  电气__采矿 编辑奇安信(360企业安全)  |  前端 编辑 zhangzhaosong.com/ 编辑
编辑

自信自律, 终身学习.
躬身入局, 皆为吾辈.

个人动态

lulu_up 发布了文章 · 4月11日

关于从入门three.js到做出3d地球这件事(第四篇: 贴图地球)

关于从入门three.js到做出3d地球这件事(第四篇: 贴图地球)

相关代码可以由此github查看

本篇介绍

     通过前三篇的学习基础知识我们已经储备差不多了, 这一篇我们要做一个贴图地球, 这种地球也是不少公司现在在使用的方案, 但缺点也比较明显就是它无法精准的选中某个国家, 所以一些精细的操作它做不到, 但是学习这个技术依旧是一件令人愉快的事情, 也没准你不需要选中国家的功能, 闲言少叙我们全军出击吧。

1. 绘制一个木块

     我们这一篇只讨论规则的矩形木块, 生活中更常见的不规则木块我们在3d模型篇再聊, 绘制木块的原理就是先生成geometry, 把它的材质定义为木头图片, 使其材质均匀有规则的分布在geometry表面, 这样在我们眼里就成了木块。

下载一个木块的图片

     我是直接百度的一张木头纹理图片, 你也可以用我这张, 同时新建一个img文件夹用来存放图片。
image

新的概念 "加载器"
const loader = new THREE.TextureLoader();

     上面代码我们生成了一个加载器, 可以用这个实例进行一系列的加载操作, 内部使用ImageLoader来加载文件, 顾名思义Texture是纹理的意思所以可以叫它纹理加载器,后面章节降到加载3d模型的时候还会介绍更多的加载器
image.png

const loader = new THREE.TextureLoader();
        loader.load(
            './img/木块.jpeg',
            (texture) => {
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )
  1. 第一个参数要加载的资源的路径。
  2. 第二个参数加载成功后的回调, 会返回纹理对象。
  3. 第三个参数进度, 将在加载过程中进行调用。参数为XMLHttpRequest实例,实例包含total和loaded字节, 请注意three.js r84遗弃了TextureLoader进度事件, 我们其实可以填undefined
  4. 第四个参数错误的回调。

当前我们直接打开我们的html文件他会报如下的错误:

image.png

     每次遇到这种跨域报错, 我们第一时间应该想到把资源放在服务器上, 但是当前有更简洁的方式。

配置vscode插件

image.png
在我们的项目页面点击右下角的 Go live 启动一个服务。
image.png
此时我们就可以得到如下的效果:
image.png
image.png

完整代码:

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 20;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);

        const axisHelper = new THREE.AxisHelper(4)
        scene.add(axisHelper)

        // 为物体增加材质
        const loader = new THREE.TextureLoader();
        loader.load(
            './img/木块.jpeg',
            (texture) => {
                console.log(texture)
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度(已废弃)
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )

        const animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

    </script>
</body>

</html>

2. 纹理属性的详谈

     我们来谈谈纹理的几个属性吧, 木块的图片想看出差别不明显, 我们在img文件夹里面再放一张鸣人的图片。
image

代码里面我们只改路径即可。

loader.load(
    './img/螺旋丸.jpeg',
    (texture) => {
    ...//

image.png
image.png

     从上图我们可以看出, 六个面上都是完整的图片, 但是由于宽高比的不同图像被相应的压缩, 接下来我们就介绍几个比较常用的属性。

重复repeat

     我们把加载到的纹理进行处理texture.repeat.x = 0.5定义他的x轴重复值。
image.png
把它的数值调大至5。
image.png

     从上面的效果可以看得出, 这个repeat.x类似在物体x轴方向的画面个数, 也就是说0.5就是x轴方向铺满需要0.5个图片, 5就是需要5张图片才能充满, 那么与之相对的就是y轴的重复正如下图:
image.png
image.png
这看起来像个礼品盒的绳子, 那么接下来我们让这个图铺满表面。

回环wrapSwrapT

t 是图片的y轴我们设置一下:

texture.wrapT = THREE.RepeatWrapping;

image.png
同理设置x轴, 注意x轴叫s:

textureObj.wrapS = THREE.RepeatWrapping

image.png

纹理不是我们这个系列的重点就不扩展了, 有兴趣的同学自己玩一玩
完整代码如下:

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 14;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(4)
        scene.add(axisHelper)
        // 为物体增加材质
        let textureObj = null;
        const loader = new THREE.TextureLoader();
        loader.load(
            './img/螺旋丸.jpeg',
            (texture) => {
                textureObj = texture;
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度(已废弃)
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )
        const pames = {
            repeatx: 5,
            repeaty: 5,
        }
        function createUI() {
            const gui = new dat.GUI();
            gui.add(pames, "repeatx", 0, 5).name("repeatx")
            gui.add(pames, "repeaty", 0, 5).name("repeaty")
        }
        const animate = function () {
            if (textureObj) {
                textureObj.repeat.x = pames.repeatx
                textureObj.repeat.y = pames.repeaty
                textureObj.wrapT = THREE.RepeatWrapping;
                textureObj.wrapS = THREE.RepeatWrapping
            }
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        createUI()
        animate();
    </script>
</body>
</html>

3. 搭建vue项目(主线任务终于开始)

     初始化一个干净的vue项目, 这个过程我就不在这里说了, 我们就从引入three.js开始, 这里要十分注意three.js的版本很重要, 同样的逻辑在不同版本里面效果竟然不一样, 所以想要和本篇一样编写代码的同学可以和我暂时统一版本:

yarn add three@0.123.2 

App.vue改装成如下的样子

<template>
  <div id="app">
    <cc-map id="map"></cc-map>
  </div>
</template>

<script>
import ccMap from "./components/cc_map.vue";
export default {
  name: "App",
  components: {
    ccMap,
  },
};
</script>

<style>
#app {
  overflow: hidden;
  border: 1px solid #ccc;
  width: 700px;
  height: 600px;
  margin: 20px auto;
}
</style>

从上面代码可以看出, <cc-map></cc-map>这个就是我第一篇文章里提到的专门的vue组件, 接下来的篇章里我们就都是围绕着开发这个组件的功能了, 除非零散的知识点我会单开一个html文件讲, 大部分都是主线任务了。

暂时新建这样三个文件夹与文件。
image.png

4. 要使用的贴图

思否不让上传超过4M的图, 所以下面是个模糊的截图, 想看原图的盆友可以看我项目里的, 这里的图片处于assets > images的位置。

image.png

config > earth.config.js内配置两个参数。

export default {
    r: 80, // 半径
    earthBg: require("../assets/images/地图.png"), // 贴图路径
}

当前初步components > cc_map.vue的模板结构, 注意习惯引入'three'的方式。

<template>
  <div class="map" ref="map"></div>
</template>

<script>
import * as THREE from "three";
import envConifg from "../config/earth.config";

export default {
  name: "ccMap",
  data() {
    return {
    };
  },
  methods: {
  },
  mounted() {
  },
};
</script>

<style scoped>
.map {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
}
</style>

5. 把基础环境的搭建抽成方法

都是之前篇章提到的方法, 先把data数据初始化好

data() {
    return {
      scene: null,
      camera: null,
      mapDom: null,
      renderer: null,
      orbitControls: null,
      object: new THREE.Object3D(),
      axisHelper: new THREE.AxesHelper(120),
      textureLoader: new THREE.TextureLoader(),
    };
  },
第一步: 初始场景使用initTHREE (之后基本不改)
initTHREE() {
  this.renderer = new THREE.WebGLRenderer({
    antialias: true,
  });
  this.mapDom = this.$refs.map;
  this.renderer.setSize(this.mapDom.clientWidth, this.mapDom.clientHeight);
  this.renderer.setClearColor(0xffffff, 1.0);
  this.mapDom.appendChild(this.renderer.domElement);
},
第二步: 初始相机使用initCamera(之后基本不改)
initCamera() {
  this.camera = new THREE.PerspectiveCamera(
    45,
    this.mapDom.clientWidth / this.mapDom.clientHeight,
    1,
    2000
  );
  this.camera.position.z = 300;
  this.camera.up.set(0, 1, 0);
  this.camera.lookAt(0, 0, 0);
},
第三步: 初始容器使用initScene(之后基本不改)
this.scene = new THREE.Scene();
第四步: 初始辅助线使用initAxisHelper(之后基本不改)
this.scene.add(this.axisHelper);
第五步: 初始光源使用initLight(之后基本不改)
const ambientLight = new THREE.AmbientLight(0xffffff);
this.scene.add(ambientLight);

后期可以模拟太阳光照射, 到时候我们加个平型光就很像回事了。

第六步: 初始轨道使用initOrbitControls(之后基本不改)
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// ...
initOrbitControls() {
  const os = new OrbitControls(this.camera, this.renderer.domElement);
  os.target = new THREE.Vector3(0, 0, 0); //控制焦点
  os.autoRotate = false; //将自动旋转关闭
  os.enablePan = false; // 不禁止鼠标平移, 可以用键盘来平移
  os.maxDistance = 1000; // 最大外移动
  os.minDistance = 100; // 向内最小外移动
  this.orbitControls = os;
},
第七步: 初始地球背景使用initBg
  • 之后会有一张专门讲物体的绘制的, 到时候我们再详聊圆形

    initBg() {
      // 把背景图加载过来当做纹理。
      const texture = this.textureLoader.load(envConifg.earthBg);
      // 这个绘制球体
      const geometry = new THREE.SphereGeometry(envConifg.r, 50, 50);
      // 放入纹理
      const material = new THREE.MeshLambertMaterial({
        map: texture,
      });
      const mesh = new THREE.Mesh(geometry, material);
      this.scene.add(mesh);
    },
第八步: 初始渲染函数使用glRender
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.glRender);

这里肯定不能直接叫render

end: 开关模式
  mounted() {
    this.initTHREE();
    this.initCamera();
    this.initScene();
    this.initAxisHelper();
    this.initLight();
    this.initOrbitControls();
    this.initBg();
    this.glRender();
  },

image.png
image.png

     这里的贴图地图其实已经可以满足部分的需求场景了, 不要看它简单它也可以很炫的。

6. ps加文字, 但会扭曲

     贴图地球有它的局限性, 比如上面地图上现在是空空的没有相应的国家名, 但是如果我在图片中ps上国家名, 让我们看看效果。
image.png
     ps上终究不是最灵活的办法, 而且如果你仔细看会发现文字有点向上弯曲, 因为图片是附着在球体上的, 所以越靠近南北极越会聚成一个点, 所以这样加文字的模式只针对少数面积大并在赤道附近的国家有用。

7. 有意思的球体

上面我们设置的球体我们单独拿出来玩一下, 这里我们只聊前三个参数, 后面会有专门介绍几何体的文章

![image.png](/img/bVcRbTv)
  1. r就是半径, 这个决定了球体的大小。
  2. 水平分段数(沿着经线分段),最小值为3,默认值为8, 比如说一个圆圈由100个点互相线段链接组成, 那么这参数就是这个100。
  3. 垂直分段数(沿着纬线分段),最小值为2,默认值为6。

来吧展示: 当我把水平分段数变成5new THREE.SphereGeometry(envConifg.r, 5, 50);
image.png
image.png

来吧展示: 当我把垂直分段数变成5new THREE.SphereGeometry(envConifg.r, 50, 5);
image.png
image.png

8. 贴图地球的局限性

  1. 如上面所说, 很难为国家区域加名称。
  2. 无法具体的选中某个国家。
  3. 无法让某个地区高亮或者出现红色边框。
  4. 视角拉近之后有些失真。
  5. 无法悬停显示详情信息
这里是发的对比

1x
image.png
2x
image.png
3x
image.png

end.

     下一篇开始正式绘制我们的矢量3d地球了, 会涉及一些数学知识, 比如三角函数你是否已经不会背了, 那我就带你研究?
     这次就是这样, 希望和你一起进步。

查看原文

赞 10 收藏 6 评论 0

lulu_up 收藏了文章 · 4月10日

使用unpkg来读取我们的私有库的包

使用unpkg来读取我们的私有库的包

unpkg 是什么

unpkg 是一个前端常用的公共 CDN,它通过 URL 语法完成了别人 web 界面内才能达到的效果,简洁而优雅,在流行的类库、框架文档中常常能看到它的身影。

它部署在 cloudflare上,在大陆地区访问到的是香港节点。 它支持 h/2 和很多新特性,如果不考虑网络延迟的原因,性能优化较为出色。在国内一些互联网公司也有镜像,例如知乎饿了么

它能以快速而简单的方式提供任意包、任意文件,通过类似这样的 URL :

unpkg.com/:package@:version/:file

怎样使用 unpkg

unpkg.com/react@^16/umd/react.production.min.js

unpkg.com/d3

例如,我在npm发布了一个组件react-signature-phone,我可以这样访问:

unpkg.com/react-signature-phone

1569378755790

也可以使用@latest访问最新版本:

unpkg.com/react-signature-phone@latest

在网址最后添加斜线,可以查看一个包内的所有文件列表:

unpkg.com/react-signature-phone/

1569379111899

最重要的是我们可以访问npm上包里面的html页面

以飞冰的区块@icedesign/empty-content-block为例;

访问https://unpkg.com/browse/@icedesign/empty-content-block/,可以看到发布到npm后的目录如下:

1569379712504

打开package.json文件,可以发现该区块发布的时候会包含如下内容:

"files": [
    "src/",
    "build/",
    "screenshot.png"
  ],
  • screenshot.png是该区块的预览图,用于飞冰平台的图片预览
  • src是源码位置
  • build是区块打包之后的静态页面和资源

打开build,可以看到打包之后的静态资源文件列表:

1569379925554

这里就包含了该页面的静态资源,我们就可以通过unpkg来直接运行这个页面:

  • 直接点击index.html,unpkg会直接打开文件读取源码,然后浏览器的路径是这样的:https://unpkg.com/browse/@icedesign/empty-content-block@3.0.0/build/index.html,删除路径中的browse/,就可以直接访问页面了。
  • https://unpkg.com/@icedesign/empty-content-block/build/index.html

1569380263694

unpkg是不能直接读取私库的包的,所以我们需要本地架设unpkg服务器;

搭建本地unpkg服务

拉取unpkg源码

从github上的unpkg中拉取源码

$ git clone https://github.com/mjackson/unpkg.git
# 安装依赖
$ npm i
or 
$ yarn

在package.json的script添加start命令:

"scripts": {
    "build": "rollup -c",
    ...
    "watch": "rollup -c -w",
    "start":"set NODE_ENV=production&&node server.js"
  },

执行编译命令:

$ npm run build

命令运行完后会在根目录生成server.js文件;

启动服务:

$ npm run start

可以看到服务启动了:

1569382230792

测试一下是否有效:

打开http://localhost:8080/可以看到我们的自己搭建的unpkg:

1569383060277

访问@icedesign/empty-content-block测试一下:

http://localhost:8080/browse/@icedesign/empty-content-block@3.0.0/

1569383364311

我们自己搭建的unpkg已经可以正常的使用了,但是目前我们私库的npm包还是不能访问,记下来就是添加私库支持了;

unpkg添加私库支持

根目录新建npmConfig.js来存放私库包的命名空间:

//存放私库包的命名空间
export const scopes = [
    '@xianzou','@lijq'
];
/****
 * 私库地址,代理端口会解析url的端口号
 * const privateNpmRegistryURLArr = privateNpmRegistryURL.split(":");
 * const privateNpmPort = privateNpmRegistryURLArr[privateNpmRegistryURLArr.length - 1]
 * 拉取一些npm的包会返回302的情况,unpkg暂时没有处理,会不会和本地的npm源有关?
 ***/
export const privateNpmRegistryURL = 'http://192.168.0.77:7788';

//互联网npm地址
export const publicNpmRegistryURL = 'http://registry.npmjs.org';

export default scopes;

接下来就是修改修改modules/utils/npm.js文件了,思路大概如下:

  • 私库地址为http,需要修改https为http;
  • 设置我们私库的端口;
  • 根据npmConfig.js中的scopes去匹配unpkg请求的包,如果是私库的包,就走内网的npm源,如果没有匹配到,就走互联网npm地址;
预览npm.js源码

修改npm.js完毕之后,执行npm run build重新生成server.js文件,然后启动服务:npm run start

测试读取我们的私库包:

http://localhost:8080/私库包

1569395531044

私库包读取正常,再来测试一下npm包:

http://localhost:8080/@icedesign/empty-content-block@3.0.0/build/index.html

1569395575722

现在私库和公网npm都可以正常预览了;

存在问题:

  • 解析包出现302问题;

    npm.js中的fetchPackageInfo方法只处理了200和404的情况,没有处理302,有的时候拉取某些公网的资源会出现302的情况,这种情况相对较少,目前还不清楚什么原因导致的,暂时还没有处理,也没有做解决处理,如果要处理大概的思路是返回320,获取重定向的路径再次请求拉取包的数据;

  • getPackage方法解析包路径可能会出现问题;

    npm.jsgetPackage方法里拼接私库包的url可能会有问题,如果私库的包拉取不到,可以变量tarballURL调试一下该方法的代码获取的路径是不是正确的;

  • 内网如果是HTTPS暂时还没有测试,目前是HTTP;

有哪些应用场景呢?

  • 公司有npm私库,发布了很多组件,但是查找困难,如果有一个预览平台,可以直接显示缩略图,直接访问预览页面,是不是很方便;
  • 搭建自己的静态资源加速器;
查看原文

lulu_up 发布了文章 · 4月7日

vue项目'微前端'qiankun.js的实战攻略

vue项目'微前端'qiankun.js的实战攻略

本篇介绍

     关于微前端的大概念大家应该听过太多了, 这里我就大白话阐述一下, 比如我们新建三个vue工程a、b、c, a负责导航模块, b负责列表页面, c负责详情页面, 然后我们可以通过微前端技术把他们组合在一起形成一个完整项目

    本篇文章不会讲述很深入的细节操作, 但会讲述项目搭建到项目上线的全环节, 如果你这些都会了那么其他的问题就不是太大阻碍了。

     一定要明确一点, 微前端在很多场景都是不适用的, 千万不要强行使用这门技术, 在本篇文章里我会一点点的阐述什么场景不适用以及为什么不适用。
    

1. 微前端qiankun.js简简简简介

     qiankun.js是当前最出色的一款微前端实现库, 他帮我们实现了css隔离js隔离项目关联等功能, 文章的后面都会有所涉及的现在就让我们开始实战吧。

2. 本次的项目结构一主二附

     一共三个vue项目, 第一个container项目负责导航模块, 第二个web1第三个web2, container项目里面有个subapp文件夹, 里面存放着web1 & web2两个项目, 这样以后我们可以随便添加web3,web4....都放在subapp文件夹即可。
image.png

3. 安装qiankun配置项目加载规则

     在我们的容器项目container里面安装qiankun如下命令:

$ yarn add qiankun # 或者 npm i qiankun -S

     打开container项目的App.vue文件我们把导航重定义一下:
     /w1/w2路由地址分别激活web1工程与web2工程。

 <div id="nav">
  <router-link to="/">Home</router-link> |
  <router-link to="/w1">web1</router-link> |
  <router-link to="/w2">web2</router-link> |
</div>

     我们新增一个id为"box"的元素, 接下来我们引入的web1工程就会插入到这个元素中。

<div id="box"></div>
<router-view />

     把Home.vue页面代码改掉:

<template>
  <div class="home">我是`container`工程</div>
</template>

<script>
export default {
  name: "Home",
};
</script>

<style>
.home {
  font-size: 23px;
}
</style>  

此时的页面是这个样子的:
image.png

     打开container项目的main.js文件写入配置。

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vueApp2',
    entry: '//localhost:8083',
    container: '#box',
    activeRule: '/w2',
  },
  {
    name: 'vueApp1',
    entry: '//localhost:8082',
    container: '#box',
    activeRule: '/w1',
  },
]);

start();

参数解析:

  1. name: 微应用的名称,微应用之间必须确保唯一, 方便后期区分项目来源。
  2. entry: 微应用的入口也就是当满足条件的时候, 我要激活的目标微应用的地址(也可以是其他形式比如html片段, 但本篇主要讲url地址这种形式)。
  3. container: 激活微应用的时候我们要把这个目标微应用放在哪里, 上面代码的意思就是把激活的微应用放在id为'box'的元素里面。
  4. activeRule:微应用的激活规则(有很多种写法甚至是函数形式), 上面代码就是当路由地址为/w1时激活。

4. 配置子项目main.js

     以配置web1项目为例, web2与其类似, 在main.js中导出自己的生命周期函数。

import Vue from "vue";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

let instance = null;
function render() {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#web1') // 框架会拿到完整的dom结构, 所以index.html里面的id也要改一下
}

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('bootstrap');
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount() {
  render()
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  instance.$destroy()
}

web1 >public >index.html中的div元素id从app改为web1, 因为要多个项目合成一个项目, 所以id最好还是不要重复。

<!DOCTYPE html>
<html lang="">

<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">
</head>

<body>
  <div id="web1"></div>
</body>

</html>

web1vue.config.js

module.exports = {
  devServer: {
        port: 8082, // web2里面改成8083
    },
}

     现在我们要分别进入container, web1web2里面运行yarn serve命令, 但是这样运行命令真的好麻烦, 接下来我就介绍一种更工程化的写法。

5. npm-run-all

     npm-run-all是用来通过执行一条语句来达到执行多条语句的效果的插件。

$ npm install npm-run-all --save-dev

# or 

$ yarn add npm-run-all --dev

改装我们的container工程中的package.json文件。

  "scripts": {
    "serve": "npm-run-all --parallel serve:*",
    "serve:box": "vue-cli-service serve",
    "serve:web1": "cd subapp/web1 && yarn serve",
    "serve:web2": "cd subapp/web2 && yarn serve",
    "build": "npm-run-all --parallel build:*",
    "build:box": "vue-cli-service build",
    "build:web1": "cd subapp/web1 && yarn build",
    "build:web2": "cd subapp/web2 && yarn build"
  },

我解释一下:
运行: yarn serve 系统会执行scripts 里面所有的头部为serve:的命令, 所以就会实现一个命令运行三个项目, 这里顺手把build命令也写了。

其他扩展玩法:

  1. serial: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
  2. continue-on-error: 是否忽略错误,添加此参数 npm-run-all 会自动退出出错的命令,继续运行正常的
  3. race: 添加此参数之后,只要有一个命令运行出错,那么 npm-run-all 就会结束掉全部的命令
上述准备工作都做完了, 我们可以启动项目试试了。

6. 请求子项目竟然跨域

     运行起来会发现报错了:
image.png

需要在web1web2两个项目vue.config.js里加上如下配置就不报错了:

devServer: {
        port: 8082,
        // 由于会产生跨域, 所以加上
        headers: {
            'Access-Control-Allow-Origin': "*"
        }
    },

之所以会有这种跨域的报错是因为qiankun内部使用fetch请求的资源, 当前毕竟是启动了三个不同的node服务, 外部html页面请求其资源还是会跨域的, 所以需要设置允许所有源。

我们为web1web2设置一下样式, 结果如下:

image.png
image.png
image.png

  • 但这些仅仅是个开始而已, 因为各种问题马上纷至沓来。

7. 区分在是否在主应用内

     我们有时候需要单独开发web1, 此时我们并不依赖container项目, 那么我们就要把main.js改装一下:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
let instance = null;
function render() {
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#web1')
}

if (window.__POWERED_BY_QIANKUN__) {
  window.__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}
export async function bootstrap() {
  console.log('bootstrap');
}
export async function mount() {
  render()
}
export async function unmount() {
  instance.$destroy()
}

逐句解释:

  1. window.__POWERED_BY_QIANKUN__: 当前环境是否为qiankun.js提供。
  2. window.__webpack_public_path__: 等同于 output.publicPath 配置选项, 但是他是动态的。
  3. window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__:qiankun.js注入的公共路径。

判断当前环境为单独开发的环境就直接执行render方法, 如果是qiankun的容器内, 那么需要设置publicPath, 因为qiankun需要把每个子应用都区分开, 然后引入容器项目内, 这样我们就可以单独开发web1项目了。

8. 子应用路由跳转与vue-router的异步组件小bug

     在配置router的时候我们经常会将页面写成异步加载:

component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),

web2项目中的home页面, 我增加一个按钮跳到about页面:

<template>
  <div class="home">
    <button @click="jump">点击跳转到about页面</button>
  </div>
</template>

<script>
export default {
  methods: {
    jump() {
      // this.$router.push("/web2/about");
      window.history.pushState(null, null, "/w2/about");
    },
  },
};
</script>

     上述代码不可以直接用this.$router.push, 这样会与qiankun.js的路由分配冲突, 官网上说会出现404这种情况, 所以建议我们直接用 window.history.pushState

     但是这中写法在当前版本qiankun.js里面可能会有如下错误:
image.png

     这是由于动态设置的publicPath并不能满足加载异步组件chunk, 需要我们如下配置一番:(web2->vue.config.js)

publicPath: `//localhost: 8083`

就可以正常加载这个页面了:
image.png

并且此时直接刷新当前url也还可以正确显示about页面。

9. 区分开发与打包

     前面几条说的都是开发相关的设置, 这里我们要开始介绍打包的配置了, 这里会介绍原理与做法, 不会做的很细所以具体的项目开发还是要好好的封装一番。

我这里先把nginx简单配置一下, 让这个包能用。

location /ccqk/web1 {
    alias   /web/ccqk/web1;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

location /ccqk/web2 {
    alias   /web/ccqk/web2;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

location /ccqk {
    alias   /web/ccqk/container;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}

由于我之前有项目在服务器上为了方便区分, 随便写了个ccqk前缀, 那么现在目标很明确了, 我需要打一个叫ccqk的文件夹, 里面有三个包containerweb1web2

第一步: 确立打包路径
  • container -> vue.config.js

    module.exports = {
    outputDir: './ccqk/container',
    publicPath: process.env.NODE_ENV === "production" ? `/ccqk` : '/',
    };
  • web1 -> vue.config.js
const packageName = require('./package.json').name;
const port = 8082
module.exports = {
    outputDir: '../../ccqk/web1',
    publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web1' : `//localhost:${port}`,
    devServer: {
        port,
        headers: {
            'Access-Control-Allow-Origin': "*"
        }
    },
    configureWebpack: {
        // 需要以包的形式打包, 挂载window上
        output: {
            library: `${packageName}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
        },
    },
    chainWebpack: config => {
        config.plugin("html").tap(args => {
            args[0].minify = false;
            return args;
        });
    }
};
  • web2 -> vue.config.json

    const packageName = require('./package.json').name;
    const port = 8083
    module.exports = {
    outputDir: '../../ccqk/web2',
    publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web2' : `//localhost:${port}`,
    devServer: {
        port,
        headers: {
            'Access-Control-Allow-Origin': "*"
        }
    },
    configureWebpack: {
        output: {
            library: `${packageName}-[name]`,
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
        },
    },
    chainWebpack: config => {
        config.plugin("html").tap(args => {
            args[0].minify = false;
            return args;
        });
    }
    };

    知识点注意解释:

  1. output.library: 配置导出库的名称, 如果libraryTarget设置为'var'那么主应用可以直接用window访问到。
  2. output.libraryTarget:这里设置为umd意思是在 AMD 或 CommonJS 的 require 之后可访问。
  3. output.jsonpFunction:webpack用来异步加载chunk的JSONP 函数。
  4. chainWebpack: 用来修改webpack的配置, 配置不进行压缩。
第二步: 配置路由路径
  • web2 -> router ->index.js

    const router = new VueRouter({
    mode: "history",
    base: process.env.NODE_ENV === "development" ? '/w2' : '/ccqk/w2',
    routes,
    });

10. css隔离

image.png
     这里的隔离并不是完美的, 想要了解更详细的内容可以看看我的往期文章带你走进-\>影子元素(Shadow DOM)&浏览器原生组件开发(Web Components API ), 看完你就会完全理解为啥不完美。

11. js隔离

     在多应用场景下,每个微应用的沙箱都是相互隔离的,也就是说每个微应用对全局的影响都会局限在微应用自己的作用域内。比如 A 应用在 window 上新增了个属性 test,这个属性只能在 A 应用自己的作用域通过 window.test 获取到,主应用或者其他微应用都无法拿到这个变量。

     我这里就不秀源码不扯大概念, 直接来干货原理, qiankun会在子应用激活的时候为其赋予一个代理后的window对象, 用户操作这个window对象的每一步都会被记录下来, 方便在卸载子应用时还原全局window对象, 你要问如何替换的window对象, 其实它是用withevel来实现的替换, 并且比如jq在执行前为了提高效率都会把window对象传入函数里使用, 那么这里直接传入代理window就都ok了, 电脑越写越卡就不扯太多了。

     所以其实使用了微前端技术方案是要付出一定的成本的, 代码速度肯定是有所降低。

12. 康威定律

  • 第一定律 组织沟通方式会通过系统设计表达出来。
  • 第二定律 时间再多一件事情也不可能做的完美,但总有时间做完一件事情。
  • 第三定律 线型系统和线型组织架构间有潜在的异质同态特性。
  • 第四定律 大的系统组织总是比小系统更倾向于分解。

     只有最适合的组织模式, 没有绝对的模式, 比如一个团队想要试试微前端, 那么其实如果你是个移动端的商城项目, 没什么必要使用微前端, 如果是个小中型的后台系统, 也不是很推荐, 除非你们是一个长期维护并且模块繁多, 或者是你想在这个项目的基础上另启一个项目做, 那么微前端将是一把神器。

end.

这次就是这样, 希望与你一起进步。

查看原文

赞 18 收藏 17 评论 1

lulu_up 收藏了文章 · 4月6日

【译】让React组件如文档般展示的6大工具

原文 6 Tools for Documenting Your React Components Like a Pro

如果没有人能够理解并找到如何使用我们的组件,那它们有什么用呢?

React鼓励我们使用组件构建模块化程序。模块化给我们带来了非常多的好处,包括提高了可重用性。然而,如果你是为了贡献和复用组件,最好得让你的组件容易被找到、理解和使用。你需要将其文档化。

目前,使用工具可以帮助我们实现自动化文档工作流程,并使我们的组件文档变得丰富、可视化和可交互。有些工具甚至将这些文档组合为共享组件的工作流程的组成部分。

为了轻而易举地将我们的组件文档化,我收集了一些业界流行的工具,如果你有推荐的组件也可以评论留言。

1. Bit

共享组件的平台

clipboard.png

Bit不仅是一个将组件文档化的工具,它还是一个开源工具,支持你使用所有文件和依赖项封装组件,并在不同应用程序中开箱即用地运行它们。
Bit,你可以跨应用地共享和协作组件,你所有共享组件都可以被发现,以便你的团队在项目中查找和使用,并轻松共享他们自己的组件。

clipboard.png
在Bit中,你共享的组件可以在你们团队中的组件共享中心找到,你可以根据上下文、bundle体积、依赖项搜索组件,你可以非常快地找到已经渲染好的组件快照,并且选择使用它们。

浏览bit.dev上的组件

clipboard.png
当你进入组件详情页时,Bit提供了一个可交互的页面实时渲染展示组件,如果该组件包含js或md代码,我们可以对其进行代码修改和相关调试。

找到想要使用的组件后,只需使用NPM或Yarn进行安装即可。你甚至可以使用Bit直接开发和安装组件,这样你的团队就可以协作并一起构建。

clipboard.png

通过Bit共享组件,就不需要再使用存储库或工具,也不需要重构或更改代码,共享、文档化、可视化组件都集中在一起,并且也能实现开箱即用。

快速上手:
Share reusable code components as a team · Bit
teambit/bit

2. StoryBook & Styleguidist

StoryBook和StyleGuidist是非常棒的项目,可以帮助我们开发独立的组件,同时可以直观地呈现和文档化它们。

clipboard.png

StoryBook 提供了一套UI组件的开发环境。它允许你浏览组件库,查看每个组件的不同状态,以及交互式开发和测试组件。在构建库时,StoryBook提供了一种可视化和记录组件的简洁方法,不同的AddOns让你可以更轻松地集成到不同的工具和工作流中。你甚至可以在单元测试中重复使用示例来确认细微差别的功能。

clipboard.png

StyleGuidist是一个独立的React组件开发环境并且具备实时编译指引。它提供了一个热重载的服务器和即时编译指引,列出了组件propTypes并展示基于.md文件的可编辑使用示例。它支持ES6,Flow和TypeScript,并且可以直接使用Create React App。自动生成的使用文档可以帮助Styleguidist作为团队不同组件的文档门户。

类似的工具还有UiZoo

3. Codesandbox, Stackblitz & friends

组件在线编译器是一种非常巧妙的展示组件和理解他们如何运行的工具。当你可以将它们组合为文档的一部分(或作为共享组件的一部分)时,在线编译器可帮助你快速了解代码的工作方式并决定是否要使用该组件。

clipboard.png

Codesandbox是一个在线编辑器,用于快速创建和展示组件等小项目。创建一些有趣的东西后,你可以通过共享网址向他人展示它。CodeSandbox具有实时预览功能,可以在你输入代码时显示运行结果,并且可以集成到你的不同工具和开发工作流程中去。

clipboard.png

Stackblitz是一个由Visual Studio Code提供支持的“Web应用程序在线IDE”。与Codesnadbox非常相似,StackBlitz是一个在线IDE,你可以在其中创建通过URL共享的Angular和React项目。与Codesandbox一样,它会在你编辑时自动安装依赖项,编译,捆绑和热重载。

其他类似工具:
11 React UI Component Playgrounds for 2019

4. Docz

clipboard.png

Docz使你可以更轻松地为你的代码构建Gtabsy支持的文档网站。它基于MDX(Markdown + JSX),即利用markdown进行组件文档化。基本上,你可以在项目的任何位置编写.mdx文件,Docz会将其转换并部署到Netlify,简化你自己设计的文档门户的过程。非常有用不是吗?
pedronauck / docz

5. MDX-docs

clipboard.png

MDX-docs允许你使用MDX和Next.js记录和开发React组件。您可以将markdown与内联JSX混合以展示React组件。像往常一样写下markdown并使用ES导入语法在文档中使用自定义组件。内置组件会将JSX代码块渲染为具有可编辑代码并提供实时预览功能,由react-live提供支持。

jxnblk / MDX-文档

6. React Docgen

clipboard.png

React DocGen是一个用于从React组件文件中提取信息的CLI和工具箱,以便生成文档。它使用ast-types@babel/parser将源解析为AST,并提供处理此AST的方法。输出/返回值是JSON blob/JavaScript对象。它通过React.createClassES2015类定义或功能(无状态组件)为React组件提供了一个默认的定义。功能十分强大。

reactjs/react-docgen
callstack/component-docs

查看原文

lulu_up 发布了文章 · 4月1日

带你走进->影子元素(Shadow DOM)&浏览器原生组件开发(Web Components API )

带你走进->影子元素(Shadow DOM)&浏览器原生组件开发(Web Components API )

image.png

本篇介绍

    习惯了使用vuereact等框架来开发组件, 但其实我们可以不依赖任何框架, 直接原生开发组件, 所以这个原生api的一大优点就是可以不依赖任何的框架。

    浏览器本身支持组件是大趋势, 但是目前使用起来并不够好, 但这并不能阻挡我们学习的脚步与对知识的好奇心, 而且我也相信原生组件几年后会成为一种主流的组件编写方式, 现在就让我们一起来学习它吧。

1. 兼容性

Chrome 54 Safari 10.1 Firefox 63

MDN上显示:
image.png

    不建议直接上生产环境。

2. 影子元素

     还记得是我第一次用qiankun.js框架的时候看到的这个概念(接下来的文章会写微前端相关实战), 这个技术可以实现一部分的css样式隔离, 之所以说只是实现一部分样式隔离, 学完这篇文章你就懂了。

第一步: 生成影子元素

    我们新建一个html5页面, 写上如下结构

<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    #cc-shadow {
      margin: auto;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
    }
  </style>
</head>
<body>
  <div id="cc-shadow">
     <span>我是内部元素</span>
  </div>
  <script>
   const oShadow = document.getElementById("cc-shadow");
   const shadow = oShadow.attachShadow({mode: 'open'});  
 </script>
</body>
</html>

    奇怪的一幕出现了, 内部元素不可见并且在查看结构的控制台里出现了特殊的结构定义。
image.png

  1. attachShadow方法给指定的元素挂载一个Shadow DOM
  2. mode: open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM。
  3. mode: open针对是dom.shadowRoot方法, 直接getElementsByClassName获取还是可以获取到的(这条很重要, 有的文章都说错了)。
  4. mode: open对应的是mode: close
  5. 注意: 不可以先开后关这种操作
第二步: 往里面注入元素
 const link = document.createElement("a");
    link.href = 'xxxxxxxxxxxx';
    link.innerHTML =  '点我跳转';
    shadow`.appendChild(link);
  1. 注意这里使用的是shadow, 而不是dom本身。
第三步: 往里面注入样式
 const styles = document.createElement("style");
 styles.textContent = `* { color:red  } `
 shadow.appendChild(styles);
  1. 通过上面可以看出, 创建了一个style标签插入了进去。
  2. 与此类似我们可以创建一个link标签插入进来效果也是一样的。

效果如下:
image.png

第四步: 样式隔离实验
 styles.textContent = `
       * { color:red  } 
       body {
         background-color: red;
       }
    `

    这里我们在影子元素内部改变了body的样式, 而这个样式没有作用到外面的body身上。
image.png

第五步: 样式渗透实验

    通过上面操作你是不是感觉这个沙盒能完美隔离css了? 那我们现在对最外层的style标签里面加上字体大小的样式, 因为影子元素无法隔离可继承的样式。

* {
    font-size: 40px;
  }

效果如下:
image.png

总结一下:

     影子元素确实可以防止样式泄露到外面污染全局, 但是也没法避免被全局的样式渗透污染, 这里的渗透指的是可继承的样式, 比如你外面style用id获取影子里面的元素改变border属性那就是无效的, 所以qiankun.js暂时无法完美隔离样式, 比如想要改变全局样式就需要靠js帮忙。
     有了上述的知识储备, 就让我们来迎接下一位主角原生组件

完整代码(来复制玩玩吧):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #cc-shadow {
      margin: auto;
      border: 1px solid #ccc;
      width: 200px;
      height: 200px;
    }
    * {
      font-size: 40px;
    }
  </style>
</head>
<body>
  <div id="cc-shadow">
    <span>我是内部元素</span>
  </div>
  <script>
    // 1: 生成影子元素
    const oShadow = document.getElementById("cc-shadow");
    const shadow = oShadow.attachShadow({ mode: 'open' });
    // 2: 注入元素
    const link = document.createElement("a");
    link.href = 'xxxxxxxxxxxx';
    link.innerHTML = '点我跳转';
    shadow.appendChild(link);
    // 3: 输入样式
    const styles = document.createElement("style");
    styles.textContent = `
       * { color:red  } 
       body {
         background-color: red;
       }
    `
    // 4: 插入使用,  可以使用插入link的方式插入css, 效果相同
    shadow.appendChild(styles);
  </script>
</body>
</html>

3. 原生组件的使用

    下图是我做的一个原生组件, 并且附上了使用方法。
image.png

 <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
 <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>

上面组件的使用看起来与vue等框架里面的组件差不多, 但是它可是很娇气的!

注意事项
  1. 自定义元素的名称必须包含连词线,用与区别原生的 HTML 元素。所以,<cc-mw>不能写成<ccMw>
  2. 如果如下方式书写去掉结尾闭合标签, 只会显示第一个, 第二个没有被渲染(这个真的好奇怪), 第二个组件会默认被插到第一个组件中, 由于被插入影子元素所以不显示了。

    <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/>
    <cc-mw name="大魔王2" image="../imgs/利姆露.webp"/>

    image.png

    奇奇怪怪的现象不是这次的主题, 我们继续研究干货。

4. 编写组件第一步template

template里面的dom结构就相当于影子元素的结构内部。

  <template id="ccmw">
    <style>
      :host {
        border: 1px solid red;
        width: 200px;
        margin-bottom: 10px;
        display: block;
        overflow: hidden;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
  </template>

知识点逐一解释:

第一个: dom定义

    上面代码我们拉倒最下面, 在这里我们可以正常的定义dom, 放心书写吧与外面写法一样。

第二个: 定义id

     <template id="ccmw">这句是让写我们可以找到这个模板。

第三个: <style>标签

    我们可以当成template标签内部就是一个影子元素的结构内部, 所以这里可以插入样式标签, 并不用js协助。

第四个: :host

    选择包含使用这段 CSS 的Shadow DOM的影子宿主, 也就是组件的外壳父元素。

5. 组件类

    编写一个组件当然需要逻辑代码啦, 该js闪亮出场了。

  <script>
    class CcMw extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow({ mode: 'closed' });
        var templateElem = document.getElementById('ccmw');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        shadow.appendChild(content);
      }
    }
    window.customElements.define('cc-mw', CcMw);
  </script>

知识点逐一解释:

第一个: HTMLElement

截取w3school上面的定义, 由此可知这个父类赋予了组件dom元素的基础属性。
image.png

第二个: 老朋友attachShadow

    把dom变成影子容器, 这样组件就可以独立出来了。

第三个: templateElem.content.cloneNode(true)

    克隆出模板里的元素, 之所以是克隆因为组件会被复用。

第四个: window.customElements.define('cc-mw', CcMw);

     组件名类名相互绑定, 官方的话就是该对象可用于注册新的自定义元素并获取有关以前注册的自定义元素的信息

第五个: 组件内部获取外部元素

    组件内是可以获取大外部元素的, 所以可以对全局进行操作, 要慎用哦。

    我们甚至可以直接把组件插入到 body中, 请注意允许, 但不提倡。

第六个: this是谁

    this就是元素本身啦。
image.png

学完影子元素是不是就很轻松理解上面的操作都是在干嘛了, 开不开心。

附上完整代码大家一起玩玩:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>原生组件</title>
  <style>
    /* 不会影响内部的样式 */
    .name {
      border: 2px solid red;
    }
  </style>
</head>

<body>
  <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
  <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
  
  <template id="ccmw">
    <style>
      :host {
        display: block;
        overflow: hidden;
        border: 1px solid red;
        width: 200px;
        margin-bottom: 10px;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
  </template>

  <script>
    class CcMw extends HTMLElement {
      constructor() {
        super();
        var shadow = this.attachShadow({ mode: 'closed' });
        var templateElem = document.getElementById('ccmw');
        var content = templateElem.content.cloneNode(true);
        content.querySelector('img').setAttribute('src', this.getAttribute('image'));
        content.querySelector('.container>.name').innerText = this.getAttribute('name');
        shadow.appendChild(content);
      }
    }
    window.customElements.define('cc-mw', CcMw);
  </script>
</body>
</html>

6. 集成为一个js文件

     上面的代码有个问题, 就是怎么组件代码与业务代码放在了一起, 当然我们可以通过技巧把他们拆散, 这里使用的是模板字符串js生成模板动态插入。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>原生组件</title>
  <style>
    /* 不会影响内部的样式 */
    .name {
      border: 2px solid red;
    }
  </style>
</head>

<body>
  <cc-mw name="大魔王1" image="../imgs/利姆露.webp"/></cc-mw>
  <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
  
  <script data-original="./2.拆散.js"></script>
</body>
</html>

上面代码清爽了很多, 下面我们可以专心写一个插件了:
./2.拆散.js

const template = document.createElement('template');
 
template.innerHTML = `
  <style>
      :host {
        border: 2px solid red;
        width: 200px;
        margin-bottom: 10px;
        display: block;
        overflow: hidden;
      }

      .image {
        width: 70px;
        height: 70px;
      }

      .container {
        border: 1px solid blue;
      }

      .container>.name {
        font-size: 20px;
        margin-bottom: 5px;
      }
    </style>

    <img class="image">
    <div class="container">
      <p class="name"></p>
    </div>
`

class CcMw extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow({ mode: 'closed' });
    var content = template.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    shadow.appendChild(content);
  }
}
window.customElements.define('cc-mw', CcMw);

7. 动态修改数据

     不能修改数据怎么能叫组件那, 这里我们要利用类的方法。

组件类添加方法:
 class UserCard extends HTMLElement {
  constructor() {
    // ...
    this.oName = content.querySelector('.container>.name');
    // ...
    shadow.appendChild(content);
  }
  // 添加方法动态改变name
  changeName(name){
    this.oName.innerText = name
  }
}

我们在使用组件的页面使用如下代码:(注意: 这里为第一个组件加了id)

 <cc-mw name="大魔王1" id="mw" image="../imgs/利姆露.webp"/></cc-mw>
 <cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>
 
 <script data-original="./2.拆散.js"></script>
 
 <script>
     const mw = document.getElementById('mw');
     setTimeout(()=>{
       mw.changeName('修改后的魔王');
     }, 1000)
  </script>

image.png

其他的修改方法其实就如出一辙了。

8. slot插槽

在模板代码里面加上: (如果不传就显示默认文案)

 <div class="container">
      <p class="name"></p>
      <slot name="msg">默认文案</slot>
    </div>

使用的时候:

<cc-mw name="大魔王1" id="mw" image="../imgs/利姆露.webp"/>
     <span slot="msg">进化了</span>
</cc-mw>
<cc-mw name="大魔王2" image="../imgs/利姆露.webp"></cc-mw>

效果如下:

image.png

end.

     这门技术可能暂时没必要太深研究, 但是学会这门知识可以使我们有更广阔的技术视野, 不断学习总是会有用的, 这次就是这样, 希望和你一起进步。

查看原文

赞 17 收藏 10 评论 3

lulu_up 发布了文章 · 3月27日

关于从入门three.js到做出3d地球这件事(第三篇: 光与影)

关于从入门three.js到做出3d地球这件事(第三篇: 光与影)

本篇介绍

     通过前面几篇我们了解了坐标系、相机、物体等概念, 这一篇我们要让3d世界里的物体, 更像我们的现实世界的物体, 我们要为3d世界绘制光与影。

1. 高级材料

     如果你看过前两篇文章, 你会发现在生成物体材质的时候我们用的是MeshBasicMaterial, basic这单词意思是基本的,那也就是说与其相对还会有高级属性, MeshLambertMaterial就是高级属性中的一种。

     使用这个属性创建出来的物体, 会产生暗淡不光亮的表面(你可以理解为需要光照时, 它的颜色才会被看到), 本篇我们一起看看它的神奇之处。

2. 物体、墙面、地面

     绘制光源之前, 我们先搭建一套环境, 这个环境很简单有物体、墙面、地面, 我们通过上一篇已经学过如何绘制一个长方体, 那么我们就以薄薄的长方体作为墙面, 最终效果如下。
image.png

     物体、墙面、地面他们身上会有辅助线, 这个是使用的:

const edges = new THREE.BoxHelper(cube, 0x00000);
scene.add(edges);
  1. BoxHelper给立方体设置边框。
  2. cube需要设置边框的物体, 后面紧跟着边框的颜色
  3. edges将实例放入场景中。

全部代码如下(../utils/OrbitControls.js的内容在我笔记里):

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 40;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);

        const cube = initCube({
            color: 'red',
            len: [1, 2, 3],
            position: [-0.5, 5, 1.5]
        })
        const wall = initCube({
            color: 'gray',
            len: [0.1, 10, 20],
            position: [-10.1, 5, 0]
        })
        const land = initCube({
            color: 'gray',
            len: [20, 0.1, 20],
            position: [0, 0, 0]
        })
        scene.add(cube);
        scene.add(wall);
        scene.add(land);

        var animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

        function initCube(options) {
            const geometry = new THREE.BoxGeometry(...options.len);
            const material = new THREE.MeshBasicMaterial({ color: options.color });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.add(new THREE.Vector3(...options.position))
            scene.add(new THREE.BoxHelper(cube, 0x00000));
            return cube
        }
    </script>
</body>
</html>

代码与之前几篇的代码没什么区别, 就是封装了一个initCube方法来创建立方体。

当我们把代码里的MeshBasicMaterial替换为``时如图:
image.png

3. AmbientLight 自然光 or 环境光

    我们的第一个主角终于登场了, 下面介绍把光源加入场景的方法。

const light = new THREE.AmbientLight('blue');
scene.add(light)
  1. new THREE.AmbientLight('blue')生成实例时传入光的颜色, 上面是蓝色的光。
  2. 放入场景中。

效果就变成了下面怪异的样子:
image.png

地面与墙壁变为了蓝色, 但是在蓝色的光照耀下红色的立方体却是黑色的。

光的颜色符合物理学

红色的物体不能反射蓝色的光, 灰色的物体却能反射蓝色的光。

  1. 自然光符合物理学, 不好计算。
  2. 自然光源没有特别的来源方向,不会产生阴影。
  3. 不能将其作为场景中唯一的光源, 但可以配合其他光源, 起到弱化阴影或给场景添加一些额外的颜色的作用。
  4. 自然光不需要指定位置它会应用到全局。

我们使用红光的时候:
image.png

所以要记住, 一些文章说与自然光颜色不同的物体都变为黑色是错的!!!

4. PointLight点光源

     顾名思义他是一个光点, 有人把它比喻成引火虫或是小灯泡, 它向四面八方发射光芒, 光源本身是不可见的所以在我们绘制的时候会在点光源的位置放置一个立方体表示其位置信息。

const light = new THREE.PointLight('white');
light.intensity = 1.8;
light.distance = 30;
light.position.set(2, 8, -5);
scene.add(light)

点光源的属性介绍:

  1. intensity光强, 想要成为最亮的星。
  2. distance光源照射的距离, 默认值为0也就是无限。
  3. visible布尔值, 是否打开光源。
  4. decay衰减值, 越大衰减速度越快。

面上代码的效果如图:
image.png
换个角度看看:
image.png
当我们把光强加大到3, 明显可以看到区别:
image.png

点光源照耀四面八方, 如果生成阴影的话计算量太大, 所以不建议开启阴影。

全部代码:

<html>

<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 40;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);

        const cube = initCube({
            color: 'red',
            len: [1, 2, 3],
            position: [-0.5, 5, 1.5]
        })
        const wall = initCube({
            color: 'gray',
            len: [0.1, 10, 20],
            position: [-10.1, 5, 0]
        })
        const land = initCube({
            color: 'gray',
            len: [20, 0.1, 20],
            position: [0, 0, 0]
        })
        scene.add(cube);
        scene.add(wall);
        scene.add(land);

        const light = new THREE.PointLight('white');
        light.intensity = 3; // 光强
        light.distance = 30; // 衰减距离
        light.position.set(2, 8, -5);
        scene.add(light)

        const edges = initCube({
            color: 'red',
            len: [0.2, 0.2, 0.2],
            position: [2, 8, -5]
        })
        scene.add(edges);

        const animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

        function initCube(options) {
            const geometry = new THREE.BoxGeometry(...options.len);
            const material = new THREE.MeshLambertMaterial({ color: options.color });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.add(new THREE.Vector3(...options.position))
            scene.add(new THREE.BoxHelper(cube, 0x00000));
            return cube
        }
    </script>
</body>
</html>

5. 生成影子的定义

     想要生成影子可不是那么简单的, 因为可想而知在数学方面影子的计算量必然很大的, 在three.js中物体是否可以显示影子是需要单独定义的。

第一步: 渲染器支持

如果使用的WebGLRender 渲染器, 需要如下开启渲染器支持。

const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
第二步: 为设置可生成阴影属性
light.castShadow = true;
第三步: 为物体设置可生成阴影属性
cube.castShadow = true;
第四步: 为物体设置可接收阴影属性
cube.receiveShadow = true;

这里注意了, 比如说a物体产生阴影, 阴影映在b物体上, 那么a与b都要设置上述的属性。

6. SpotLight 聚光灯(有方向的光)

     这个光源是有方向的, 也就是说他可以指定照向谁, 并且可以产生阴影。

let light = new THREE.SpotLight("#ffffff");
    light.position.set(1, 1, 1);
    light.target = cube
    scene.add(light);

可配置的属性与上面的基本相似, 多了一个target:
target指定照谁, target必须是一个THREE.Object3D对象, 所以我们经常会先创建一个Object3D对象, 让它不可见然后光源就可以通过照射它, 从而实现任意方向。

我们先看一下光源在上方照射, 下方物体产生阴影的效果:
image.png

7. SpotLight 模拟手电(锥形光)

    开发中我们会用SpotLight模拟手电与灯光, 可以利用他的angle角度属性。

const light = new THREE.SpotLight("#ffffff");
scene.add(light);

image.png

当我们把背景颜色换成黑色的效果:
image.png
上图就如同黑夜里手电照射的效果了。

为了方便调试聚光灯,官方给了我们专属的辅助线。
const helper = new THREE.CameraHelper(light.shadow.camera);
    scene.add(helper);

image.png

下面我们标注一下都有哪些知识点:
image.png
image.png
    重要的是有了这些辅助线我们就知道如何优化自己的项目了, 比如减小光源的远平面。

完整代码

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 40;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        renderer.shadowMap.enabled = true;
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);
        const cube = initCube({
            color: 'red',
            len: [3, 1, 3],
            position: [0, 2, 0]
        })
        const wall = initCube({
            color: 'gray',
            len: [0.1, 10, 20],
            position: [-10.1, 5, 0]
        })
        const land = initCube({
            color: 'gray',
            len: [20, 0.1, 20],
            position: [0, 0, 0]
        })
        scene.add(cube);
        scene.add(wall);
        scene.add(land);
        const arr = [8, 8, 0]
        const light = new THREE.SpotLight("#ffffff", 1);
        light.intensity = 2.5;
        light.position.set(...arr);
        light.castShadow = true;
        light.target = cube
        light.decay = 2;
        light.distance = 350;
        light.angle = Math.PI / 5
        light.penumbra = 0.05;
        scene.add(light);
        // 聚光灯助手
        const helper = new THREE.CameraHelper(light.shadow.camera);
        scene.add(helper);
        const edges = initCube({
            color: 'red',
            len: [0.2, 0.2, 0.2],
            position: [...arr]
        })
        scene.add(edges);

        const animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

        function initCube(options) {
            const geometry = new THREE.BoxGeometry(...options.len);
            const material = new THREE.MeshLambertMaterial({ color: options.color });
            const cube = new THREE.Mesh(geometry, material);
            cube.castShadow = true;
            cube.receiveShadow = true;
            cube.position.add(new THREE.Vector3(...options.position))
            scene.add(new THREE.BoxHelper(cube, 0x00000));
            return cube
        }
    </script>
</body>
</html>

8. DirectionalLight 平型光

     经常被举例子的就是太阳光, 实际上太阳光也不是平行的, 只是距离太远了几乎可以算是平行。
     这个光源与其他的不同的点是, 他它所照耀的区域接收到的光强是一样的。

const light = new THREE.DirectionalLight("#ffffff");
scene.add(light);

介绍几个新属性:

light.shadow.camera.near = 5; //产生阴影的最近距离
light.shadow.camera.far = 50; //产生阴影的最远距离
light.shadow.camera.left = -3; //产生阴影距离位置的最左边位置
light.shadow.camera.right = 3; //最右边
light.shadow.camera.top = 3; //最上边
light.shadow.camera.bottom = -3; //最下面

image.png

通过上图我们可以得知, 这个光源是完全平行的。

完整代码如下:

<html
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 40;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x000)
        renderer.shadowMap.enabled = true;
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);

        const cube = initCube({
            color: 'red',
            len: [3, 1, 3],
            position: [0, 2, 0]
        })
        const wall = initCube({
            color: 'gray',
            len: [0.1, 10, 20],
            position: [-10.1, 5, 0]
        })
        const land = initCube({
            color: 'gray',
            len: [20, 0.1, 20],
            position: [0, 0, 0]
        })
        scene.add(cube);
        scene.add(wall);
        scene.add(land);

        const light = new THREE.DirectionalLight("#ffffff");
        light.intensity = 1.5;
        light.position.set(8, 8, 0);
        light.castShadow = true;
        light.target = cube
        light.shadow.camera.near = 5; //产生阴影的最近距离
        light.shadow.camera.far = 50; //产生阴影的最远距离
        light.shadow.camera.left = -3; //产生阴影距离位置的最左边位置
        light.shadow.camera.right = 3; //最右边
        light.shadow.camera.top = 3; //最上边
        light.shadow.camera.bottom = -3; //最下面
        scene.add(light);
        // 聚光灯助手
        const helper = new THREE.CameraHelper(light.shadow.camera);
        scene.add(helper);

        const animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

        function initCube(options) {
            const geometry = new THREE.BoxGeometry(...options.len);
            const material = new THREE.MeshLambertMaterial({ color: options.color });
            const cube = new THREE.Mesh(geometry, material);
            cube.castShadow = true;
            cube.receiveShadow = true;
            cube.position.add(new THREE.Vector3(...options.position))
            scene.add(new THREE.BoxHelper(cube, 0x00000));
            return cube
        }
    </script>
</body>
</html>

9. 光源要配合使用

     一般情况下不会只有单一光源, 比如我们会先放一个环境光, 然后在灯的模型中放上其他光源, 一些rpg游戏会用聚光灯处理用户视角。

     我们可以同时使用多个光源, 利用gui.js查看各种绚丽的效果, 比如我们可以用束平型光模拟舞台效果。

end.

下章会从绘制一个木块开始, 最后绘制一个贴图地球, 这次就是这样希望与你一起进步。

查看原文

赞 28 收藏 14 评论 5

lulu_up 发布了文章 · 3月21日

关于从入门three.js到做出3d地球这件事(第二篇: 开发必备的辅助技能)

关于从入门three.js到做出3d地球这件事(第二篇: 开发必备的辅助技能)

本篇介绍

     开发3d效果的时候, 不能每次都通过刷新页面来更新图像, 我们工程师当然会发明出相应的工具辅助开发工作, 这一篇我们一起学习三个好用的工具, 让我们的开发更畅快。

     上篇我们讲解了three.js的基本配置代码, 想看的同学可以访问这个链接: 关于从入门three.js到做出3d地球这件事(第一篇: 通俗易懂的入门)

一. 相机的配置

  • 这里介绍的是透视相机

     介绍工具之前我们先把相机的关键概念系统的学一遍, 因为以后我们要利用相机做很多有趣的事。
     这里以上一篇绘制的最基本的坐标系为例进行说明, 如下图:
image.png

第一: position 相机位置

     位置属性很重要很常用, 不同的位置呈现出不同的景色, 我们可以把相机理解为我们在3d世界中的眼睛, 而调整相机的位置就相当于我们走到不同的角度去看这个3d世界。
     看过上一篇你会知道我们的相机实例叫camera, 我们对他的position属性进行设置就可以调整位置。

  • 第一种设置方式
    camera.position.x = 2;
    camera.position.y = 2;
    camera.position.z = 10;

上面就是分别调节了相机的x, y, z轴的距离, 我们看到的景象变成了下面的样子。
image.png

  • 第二种设置方式

     position身上有set方法可以设置, 三个参数对应的是x, y, z。

camera.position.set(2, 2, 10)

     效果与上面的一样。

  • 第三种设置方式

     position可以直接设置x, y, z属性, 本身又有set方法, 那么position属性本身到底是个什么那? 让我们打印出来看看。
image.png
isVector3: true也就是说它是一个Vector实例, 那么Vector是什么?
我们以后会经常和这个单词打交道, 让我们一起记住它。
image.png

  • 先不细聊向量

因为向量是个很重要的概念, 我们后面会单独大篇幅的详谈, 这里咱们单纯的理解为new THREE.Vector3(2, 2, 10)是生成了一个, 参数就是这个点的xyz坐标, 而我们相机的position属性就是这样一个对象。

  • 注意: 直接赋值是无效的

camera.position = new THREE.Vector3(2, 2, 10) 无效

需要利用add方法来实现
camera.position.add(new THREE.Vector3(2, 2, 10)) 有效

  • 别被唬住

上面展示了大部分常用的设置position的方法, 我在初学three.js的时候被网上各种写法弄晕了所以这里特意列出大部分写法, 希望当你再看其它资料的时候就不会被乱七八糟的写法唬住了。

第二: lookAt 相机看向哪里

     这个概念简直太重要了, 如其字面意思就是看向哪里, 上面相机位置已经调整完毕, 那么我们要调整相机拍摄哪里了。

默认是(0,0,0)的位置如下图:
image.png

当我们看向坐标系的 (3, 3, 0)位置也就是右上角:
image.png

     从它的效果我们可以发现, 这个属性非常适合在3d游戏中调整人物的方向时改变图像, 如果你要做第一人称游戏一个人在城市里奔跑的效果, 那无非就是不断的改变相机的positionlookAt就能做到了。

  • 设置方式

这里可以直接设置: camera.lookAt(3, 3, 0);
还可以利用向量来设置: camera.lookAt(new THREE.Vector3(3, 3, 0));

第三: up 谁为相机上方

先来一张默认的情况, 不难看出绿的是y, 红的是x, z正对着我们所以暂时看不到:
image.png

我们设置一下camera.up.set(1, 0, 0);
image.png

上面x的值成为了最大, 所以他变成了上方的坐标轴, 当然如我们设一个乱乱的值camera.up.set(1, 0.5, 0); 那么效果如下:
image.png

这个属性的设置方式就是set方法或者camera.up = new THREE.Vector3(1, 0.5, 0);

可以利用这个属性模拟第一人称游戏里任务摔倒了看这个世界....

坑点

我当前版本的three.js想要up属性生效需要在设置完up属性之后再主动指定一下camera.lookAt(0, 0, 0);否则up属性不生效;

二. GUI的使用

     上面讲了这么多, 我们现在想让场景动起来, 所以需要不断的渲染出3d图像, 我们利用requestAnimationFrame反复调用渲染函数就能实现动画效果了。

const animate = function () {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    };
animate();

-全名dat.gui.js
他的功能是为属性生成一个可调节值的面板, 方便我们不断修改数值而不用刷新页面如下图:
image.png
鼠标拖动调节
image.png

image
-引入GUI
<script data-original="https://cdn.bootcdn.net/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
引入之后我们全局多了一个dat属性。

const gui = new dat.GUI();
// 1: 定义一个我们要改变的对象
const pames = {
  x: 0
}
// 2: 把这个值放入控制器
gui.add(pames, "x", 0, 5).name("x轴的距离")

参数解答

  1. 传入要改变的对象。
  2. 要改变这个对象身上的哪个属性。
  3. 最小值
  4. 最大值
  5. .name('显示在调节栏的名称')

在每次渲染的时候更新一下相机的x轴位置。

const animate = function () {
    camera.position.x = pames.x
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();

知道上面这些就可以应付很多的场景了, 一个工具而已不用深究啦。

全部代码
<html>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 20;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x00FFFF, .5);
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(2)
        scene.add(axisHelper)
        const pames = {
            x: 0
        }
        function createUI() {
            var gui = new dat.GUI();
            gui.add(pames, "x", 0, 5).name("x轴的距离")
        }
        const animate = function () {
            camera.position.x = pames.x
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        }
        createUI()
        animate();
    </script>
</body>
</html>

三. tween的使用

     tween.js是用来做流畅动画的库, 比我们自己写动画方便多了tween官网地址

下面编写了一个相机平滑的向右上角移动的代码。

const tween = new TWEEN.Tween(camera.position).to({
    x: 10,
    y: 10
}, 2000).repeat(Infinity).start();

 // tween.stop() // 可以停止动画
 
const animate = function () {
    TWEEN.update();
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();
  1. new TWEEN.Tween("这里传入要改变的对象")
  2. .to( x: 10 y: 10}, 2000), 在2000毫秒时将x与y属性变成10。
  3. .repeat(Infinity), 这个动无限循环。
  4. .start();, 开始执行动画。
  5. .stop();, 停止动画。
  6. TWEEN.update();, 每次调用渲染函数都要调用一下动画的更新函数。

效果如下(思否暂时无法传gif图片, 但我已经向高老板反应了):
image.png
image.png

下面是动图, 显示可能有问题。
image

这个库大概的原理就是每次调用update方法的时候判断一下该动画已经执行了多久时间, 然后算出当前时间目标对象的值应该变为多少, 当然它还会对性能有所优化。

全部代码如下:
<html>
<style>
    * {
        padding: 0;
        margin: 0;
    }
</style>

<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 20;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x00FFFF, .5);
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(2)
        scene.add(axisHelper)

        const tween = new TWEEN.Tween(camera.position).to({
            x: 10,
            y: 10
        }, 2000).repeat(Infinity).start()
        const animate = function () {
            TWEEN.update()
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>

</html>

四. 轨道控制器的使用

     这个就厉害了, 让我们可以使用鼠标转动我们的相机, 仿若进入到3d世界一般。

image

随着我们按住鼠标并且移动, 视角就随之变化仿佛身临其境一般。

// 将轨道控制器的代码放在对应的文件夹里面, 如果你没找到就用下面我分享的文件。
<script data-original="./utils/OrbitControls.js"></script>

引入成功页面THREE身上会出现OrbitControls方法, 我们需要传入相机与渲染的容器。

  const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
  orbitControls.target = new THREE.Vector3(0, 0, 0);//控制焦点

     cdn上我没查到, 想要获取代码的同学可以复制我的笔记内容到项目中 three.js轨道控制器

直接在页面引入与通过npm包的方式引入有区别, 到了讲在vue里的使用的时候我们再详细说。
全部代码如下: (要有./utils/OrbitControls.js的代码, 没有的话来我笔记下载)
<html>

<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="./utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 10;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x00FFFF, .5)
        // 轨道控制器
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        // orbitControls.target = new THREE.Vector3(0, 0, 0);
        // 轨道控制器
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(2)
        scene.add(axisHelper)
        var animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();
    </script>
</body>

</html>

end.

下一篇将会介绍 光源, 与 阴影的玩法了, 希望与你一起进步。

查看原文

赞 28 收藏 17 评论 7

lulu_up 回答了问题 · 3月18日

解决vue的动态路由匹配和路由组件传参(query传参)有什么区别?

上面都说的很全面, 我再补充一个点, 比如你做秒杀页面, 页面是ssr生成好的, 直接靠id获取页面, 而不是在页面里面获取id再请求商品, 这是秒杀页面的一个优化点。

关注 4 回答 4

lulu_up 发布了文章 · 3月16日

关于从入门three.js到做出3d地球这件事(第一篇: 通俗易懂的入门)

关于从入门three.js到做出3d地球这件事(第一篇: 通俗易懂的入门)

开篇介绍

    如果你没接触过3d可视化技术, 你也许会认为可视化非常难, 光是一个物体的阴影要如何计算就相当复杂, 但是告诉你个好消息, 阴影的计算都是集成好的, 而我们只要设置好光源的位置,绘制好物体就可以了, 真的没有想象中那么复杂, 本文面向有前端基础,但零可视化基础的同学, 我会从最基础的入门知识说起。

    学习可视化方面的技术会让我们对计算机, 对前端技术有更深的理解, 还可以做出更多有趣味的东西来, 本文是我踩了好多坑后总结出来的, 我更清楚一个初入门的小白哪里不懂。

    three.jswebgl的第三方库, 它更适合不太复杂的可视化项目, 而我们要做的3d地球项目使用它来做会更简单, 所以选择了它, 放心后面也会说webgl相关知识 。

当前效果如下:
image

一. 关于此系列文章

  1. 自食其力:不管是在公司还是网上都有类似的库, 但是当遇到bug或是缺少功能的情况时就会很麻烦, 例如我们公司的FGL库(一个内网绘制3d景象的技术), 它官网上的例子很多都是错的, 使用起来也是一堆问题, 比如无法精准选择某个国家, 点击事件消融等bug。
    还比如说Echarts的地球, 它太注重真实感并且用起来有点卡, 以及交互做的不太好。
  2. 直指核心: 去年我通过看书、看文章、看视频认真的学习three.js, 并做出了3d地球这个项目, 而这个系列文章将会直指做出3d地图的核心知识, 尽量不随意扩散知识面。
  3. 更好入门: 网上的教学文章千篇一律, 点进去阅读完感觉其对于一个three.js零基础的同学来说都不太好懂, 教学视频里的知识点太广泛, 事无巨细的罗列, 而这个系列文章将更突出绘制3d地球这个重点。
  4. 同道中人: 我学习three.js就是为了做出3d地球, 期间走了不少弯路, 被某些问题卡了很久, 所以我更懂一个刚入门的人困惑的点在哪里。
  5. 专注vue: 市面上较少专门针对vue做到开箱即用的3d地球插件, 而我们就要编写这样一款产品。
  6. 不断学习: 编写文章也是我提高自己能力的一种方法, 死磕每个知识点让自己的理解更上一层楼。

二. 任务目标

  1. 入门three.js技术。
  2. 绘制出3d地球。
  3. 做成专门vue使用的库。
  4. 后期也会介绍着色器的概念与基本的使用技巧。
  5. 会介绍少量webgl的相关用法, 并且会有部分数学知识。

三. 文章主线剧情与支线任务

  • 主线剧情: 围绕着如何做出3d地球, 这部分在vue工程里面进行。
  • 支线任务: 每个分散的知识点, 可能与3d地球没关系, 但是它能帮助我们更好的理解3d技术, 而这些知识点我就不在vue项目里面演示了, 会单独创建一个html文件来演示说明。

四. 理解坐标系: 别着急写代码先有基本模型

     像绘制图形这类技术, 最基本的概念就坐标系, 下图是二维坐标系, 我们的故事就从这个家伙开始。
image.png
     我们用(0, 0)表示坐标的中心点, 绘制一条起点为中心点长度为1的线段可以使用 (0, 0) (1, 0)这两个点相连表示。

关于向量的概念后面需要用数学知识的时候再介绍, 前几篇文章就越通俗越好。

     在three.js中我们要打交道的就是下面这位三维坐标系
image.png
     他的坐标原点就是(0, 0, 0), 绘制一条起点为中心点的长度为1的线段可以是 (0, 0, 0) (1, 0, 0)

     这里要记住, three.js里面设置的默认坐标系就是这种形式x向右, y向上, z向前, 之所以说是默是因为它可以修改。

     上图中, 观看这个三维坐标系的目光其实是在斜上方, 正常情况下在我们开发的时候z轴是正对着我们的眼睛的, 所以你只能看到z轴是一个点,
image.png

在开发与学习的时候, 最好先把坐标系绘制到页面上, 方便我们更好的绘制。

五. 相机的概念

     假设现在我们的正前方有一个三维坐标系的全息投影, 那么此时你的眼睛就相当于一架相机, 你看到的 坐标系景象取决于你站的位置。

     在three.js中就有这样一个对象, 他就是负责从哪个角度观察我们绘制的3d世界, 也就是相机这个概念的由来。

     相机分为两种, 正投影相机和透视投影相机, 正投影相机就是你站的多远你看到的物体的大小都不变, 透视投影相机就是物体会近大远小, 下面是张引用图 (图片来自网络)。

image.png

正投影相机可以用在工程制图上, 或者可以做一些视觉欺骗小游戏。

本文主要目的是绘制3d地球所以主要使用透视投影相机

六. 绘制坐标系, 安放摄像机 (代码安排上)

引入three.js, 可以把包下载到本地, 也可以直接获取在cdn上的资源, 引入之后全局会出现THREE对象, 我们就可以开始编程之旅了。

    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>

一个普普通通的html空文件的script标签里面, 发生着这样的故事: 让我们逐句解析

第一步:创建场景, 也就是虚拟的空间

     我们之后绘制的3d物体都要放入这个空间里面, 你可以把它当做一个鸿蒙空间神器, 里面有一个小世界, 而我们是掌控者(很中二)。

const scene = new THREE.Scene();
第二步:创建相机

     相机的概念上面讲述过了, PerspectiveCamera这个类就是透视投影相机, 我们来逐个攻破他参数的意思。

  1. 35: 视角也就是我们左眼与右眼可以看到的横向角度, 其越小物体则越大, 因为目光变狭窄会突出物体, 你可以做一个实验, 聚精会神的盯着看一个物体, 你就会发现此时你左右两边本来靠余光可以看到的物体你现在看不清, 这个就是你的视角变小了, 变小视角还可以使目标物体比例变大, 我们知道这些就够理解这个数字了, 后期可以利用这个原理做一些令人惊讶的动画特效。
  2. window.innerWidth / window.innerHeight: 纵横比宽/高, 这里宽高不会去写px这种单位, 坐标系里面是一种抽象的长度单位, 所以要告诉浏览器咱们当前显示图像的区域的宽高比例(可以当它是百分比布局, 就像我们写css布局时使用vhvw为单位)。
  3. 1: 近平面, 简单理解就是当一个图像距离相机的距离小于1的时候, 就不显示这个图像了。
  4. 1000: 远平面, 简单理解就是当一个图像距离相机的距离大于1000的时候, 就不显示这个图像了。
  5. camera.position.z = 10; 相机的坐标不设置的话, 默认就是(0, 0, 0)坐标原点, 这样类似脑袋在坐标轴原点上看坐标轴, 所以这里要设置距离坐标中心有一定距离, 也就是远距离观察这个坐标系。
const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 10;
  • 无聊的知识: 我们在玩3d游戏的时候, 是不是有时候与另一个游戏人物距离太近了就会出现人物中空的效果, 这些很可能就是他的某些部分距离你相机的距离, 小于了近平面的距离导致的。
  • 物体距离眼睛越近越大, 越远越小, 因为一个物品无限大与无限远没有意义, 显示起来浪费性能, 所以才会设置近平面与远平面。
第三步:生成渲染实例
  1. WebGLRenderer生成一个渲染实例, 用来渲染我们所有的3d效果。
  2. setSize设置场景的宽高。
  3. setClearColor设置背景色, 这个背景色不是平面的, 是全方位的, 你可以想想成你在一个屋子里, 这个颜色就是屋子墙壁、地板、天花板的颜色(.5是透明度)。
  4. renderer.domElement生成的渲染的实例, 这个要放到对应的dom容器里面(是个canvas标签)。
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x00FFFF, .5)
document.body.appendChild(renderer.domElement);
  • 知识点: setClearColor不写就是黑色
  • 知识点: setClearColor可以直接写"red"这种, 不用必须16进制。
第四步:插入坐标系实例
  1. AxisHelper: 用于生成辅助坐标实例, 2代表这个坐标系的长度, 因为我们不一定需要多长的辅助线。
  2. scene: 老朋友场景, 它的add方法就是把某某某加入到场景中来。
const axisHelper = new THREE.AxisHelper(2)
scene.add(axisHelper)
第五步:渲染出来
  1. 第一个参数是场景, 第二个参数是相机
renderer.render(scene, camera);

下面是效果图, z轴正对着我们所以看不到:
image.png

在斜上方看到是如下的效果, 之后的章节会说如何调整相机的位置与角度
image.png

完整的代码如下

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 10;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x00FFFF, .5)
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(2)
        scene.add(axisHelper)
        renderer.render(scene, camera);
    </script>
</body>
</html>

七. 第一个立方体

     不画一个立方体感觉对不起 第一篇这个题目, 要注意了在three.js中你可以理解为绘制一个几何体需要两部分, 一个是几何体本身, 比如这个几何体的长宽高, 另一个就是材质可以简单理解为表面的颜色样式。
     geometry这个单词我们会经常打交道的, 来一起记下它吧。

image.png

BoxGeometry 长方体

const geometry = new THREE.BoxGeometry(1, 2, 3);

  1. 1: '长', 也可以理解为在不设置坐标的时候在x轴上的长度。
  2. 2: '高', 也可以理解为在不设置坐标的时候在y轴上的长度。
  3. 3: '宽', 也可以理解为在不设置坐标的时候在z轴上的长度。

new出来的实例上面会有这个几何体的点的信息, 面的信息等等, 这个后面再详细说这次主要入门。

MeshBasicMaterial 材质

颜色与上面设置setClearColor一样, 什么写法都行的, 下面是我设置了一个红色的材质。
const material = new THREE.MeshBasicMaterial({ color: 'red' });

生成'网格' Mesh

const cube = new THREE.Mesh(geometry, material);
网格上含有位置信息、旋转信息、缩放信息等等, 他需要用几何体材质两个参数, 但其实并不像网上说的必须要有材质, 不传材质也能显示。

放入场景

也就是场景对象scene本身有个add方法。
scene.add(cube);

image.png
右上方视角
image.png

放入场景的几种方式

1: 我直接放入geometry
scene.add(geometry); 会报错了, 可以理解为不是网格对象所以报错了。
以后遇到这类报错一定要考虑类型问题。
image.png

2: 未设置材质

const cube = new THREE.Mesh(geometry);

scene.add(cube);

image.png
image.png

白白的一片, 并且控制台没有报错。

八. 全部代码

<html>
<body>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script data-original="./utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 10;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x00FFFF, .5)
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(2)
        scene.add(axisHelper)

        const geometry = new THREE.BoxGeometry(1, 2, 3);
        const material = new THREE.MeshBasicMaterial({ color: 'red' });
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        renderer.render(scene, camera);
    </script>
</body>

</html>

end

     第一篇写的内容并不多, 等基本知识储备够了就可以开始编写3d地球了, 那里将会很有意思。
希望与你一起进步。

查看原文

赞 54 收藏 35 评论 7

lulu_up 发布了文章 · 1月27日

记一次前端"chrome插件"基础实战分享会(建议收藏)(下篇)

承接上文

十. 鼠标右键菜单

需要先在权限里面加上菜单的权限manifest.json "permissions": ["contextMenus"],.
具体的添加菜单的代码, 我们放在background.js文件里面

  chrome.contextMenus.create({
    title: "敢不敢点击我",
    onclick: function(){alert('点击了人家');}
  });

image.png

这里点击后就调用我们定义的点击方法了
image.png

第二种方式: 我们把菜单做成选项, 比如三种模式让用户选一种

chrome.contextMenus.create({
  title: "选项1",
  "type": "radio",
  onclick: function(){alert('使用1');}
});
chrome.contextMenus.create({
  title: "选项2",
  "type": "radio",
  onclick: function(){alert('使用2');}
});
chrome.contextMenus.create({
  title: "选项3",
  "type": "radio",
  onclick: function(){alert('使用3');}
});

image.png

当然了, 多选也是有的


chrome.contextMenus.create({
  title: "选项1",
  "type": "checkbox",
  onclick: function(){alert('使用1');}
});
chrome.contextMenus.create({
  title: "选项2",
  "type": "checkbox",
  onclick: function(){alert('使用2');}
});
chrome.contextMenus.create({
  title: "选项3",
  "type": "checkbox",
  onclick: function(){alert('使用3');}
});

image.png

右键菜单的第二大类

只有选中某些内容了才展示的菜单

  chrome.contextMenus.create({
    title: '选中了:%s', // %s表示选中的文字
    contexts: ['selection'], // 只有当选中文字时才会出现此右键菜单
    onclick: function(params)
    {
     alert('查询某个东西')
    }
  });

第一种与第二种菜单同时creat, 显示时只显示对应的一种情况
image.png

十一. content_scripts ‘恶意广告’的元凶?

  1. 什么是content_scripts

比如访问某个页面, 我会把我的代码注入到这个页面的js代码里面, 这就是content_scripts的作用, 我可以或者当前页面的dom结构以及document.

  1. 配置引入content_scripts

manifest.json, 下面这段话的意思就是所有页面都插入, "content/index.js"的代码逻辑

  "content_scripts": 
    [
        {
            "matches": ["<all_urls>"],
            "js": ["content/index.js"],
        }
  ],
  1. 插入的时机

content_scripts配置是一个Array类型, 所以说明他可以配置多组逻辑, 并且其中还可以配置代码生效的时机;
"run_at": "document_start" dom刚开始加载的时间
"run_at": "document_end" dom加载完成(此时才能获取dom)
"run_at": "document_idle" 页面空闲的时候(默认是他)

我们随便写一段代码

console.log('我注入了')
console.log('获取window', window)
console.log('获取document', document)

然后更新组件, 在不同页面看一下效果:
image.png
image.png

每个网站加载时都会执行一遍, 但是一些注意事项在第七条里面, 别忘了看看.

  1. 插入操作的dom, 可否获取源js的变量?

这里我们自己设置一个html页面, 来一一验证我们的问题.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="wo">我会被获取</div>
  <script>
    var  cc = '金毛'
  </script>
</body>
</html>

插件里面增加逻辑

console.log('我注入了')
console.log('获取window', window)
console.log('获取document', document)
console.log('获取cc', window.cc)
console.log('获取dom',document.getElementById('wo'))

image.png

这里其实不建议我们改动用户的js代码逻辑, 不管是安全因素还是整体逻辑的自洽都不建议如此操作, 这里也就不介绍可以访问js变量的方法了, 用兴趣的可以去官方网站查一查.

  1. 配置css

我们还可以再页面插入css代码, 是页面样子发生变化
manifest.json进行修改

"content_scripts": 
    [
        {
          "matches": ["<all_urls>"],
          "js": ["content/index.js"],
          "css": ["content/index.css"]
        }
  ],

css可以影响页面上的所有dom结构, 包括原本的dom和我们后插入的dom结构
content/index.css里面

#wo {
  border: 1px solid red;
}

页面效果如下
image.png

  1. 插入恶意广告与修改dom

既然获取了dom那么我们当然就可以修改dom, 或者是新增dom倒你的页面.

修改元素内容
document.getElementById('wo').innerText = '被修改后的内容'

添加元素到页面上.....

const div = document.createElement('div');
div.className = 'cc'
div.innerHTML = "是兄弟就来kanwo"

document.body.append(div);

div.addEventListener('click', ()=>{
  alert('领取999999')
})

样式也要好看一点

.cc {
  display: flex;
  color: white;
  position: fixed;
  align-items: center;
  justify-content: center;
  background-color: black;
  right: 0;
  bottom: 0;
  width: 300px;
  height: 200px;
}

实际效果就是图里这样了
image.png

  1. 权限注意:
    ①: 这个方法在插件扩展页面无效!!! 请去普通页面调试!
    ②: 获取不到本页面的变量
    ③: 页面变更不会触发重新执行, 只有页面刷新才会触发重新执行

十二. devtools自创面板, 监测网页的一举一动

这个我先举个例子你就这道这是个啥功能了
image.png

它可以分为两个部分, devtools_page可以用来操作tab栏的, panel是每一个具体的tab里面的html结构与逻辑

创建一个标签
manifest.json中增加这样的一段配置
"devtools_page": "devtools/index.html"
在这个文件里面我们定义js的引用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script data-original="./index.js"></script>
</body>
</html>

index.js文件里面写上

// 创建自定义面板,同一个插件可以创建多个自定义面板
// 几个参数依次为:panel标题、图标(不用写)、要加载的页面、加载成功后的回调
chrome.devtools.panels.create('被我占领了', null, 'panel/index.html', function()
{
  // 这个必须要关闭控制台, 再打开才能显示出来, 中文也没问题哈
  console.log('自定义面板创建成功!');
  // 他自己也是个html页面, 所以可以二次检查
});

image.png

一定要注意, 这个一定要f12关闭控制台再打开控制台才会生效, 刷新没有用的别被坑了!!

image.png
image.png

这里是控制台嵌套控制台,是不是很神奇,与我们平时的理解有点点出入感。

那么问题来了, 这第一个控制台其实可以算是一个页面, 那他的html怎么搞?? 其实就是依靠panel

panel/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script data-original="./index.js"></script>
</body>
</html>

panel/index.js

console.log('我是panel页面的控制台, 大家好')

image.png

既然它相当于一个页面, 那么肯定就可以操作他的dom结构, 我们来实现以下
操控他的dom

const nr = document.getElementById('nr');
const bt = document.getElementById('bt');
bt.onclick = function(){
  nr.innerHTML = "内容被改变了"
}

结果当然很明确了
image.png
image.png

十三. 获取被审查页面资源

panel主要的一个功能就是结合页面的情况给出一个处理分析的方案, 那么我们要如何与被审查页面进行交流那?
panel/index.js

 chrome.devtools.inspectedWindow.getResources ((res)=>{
   console.log('获取的资源元素', res)
  const obj = {}
  res.forEach((el, i) => {
    obj[el.type] = el.url + `xxxxxxx-> ${i}`
  });
  console.log('类型对象', obj)
 })

我们定义的简单页面:
image.png

网上比较复杂的页面:
image.png

有上面的信息可以看出, 这个获取被审查页面资源的api主要是获取加载的资源类型与连接, 按照主要的类型分类就那几个.
我们可以利用这个api分析项目加载的资源结构.

十四. 获取被审查页面dom

这个有点绕, 你也可以尝试其他方法获取dom, 这里介绍的是eval方法
从一个最基本的例子说起

chrome.devtools.inspectedWindow.eval("window.location", (res, isException)=>{
   console.log('执行结果是: ', res, isException)
 })
  1. chrome.devtools.inspectedWindow.eval 是在被审查页面执行的语句
  2. 第一个参数是 被执行的语句
  3. res是执行语句的返回值
  4. isException 是执行有没有报错, 如果出现异常则为true
  5. 值得注意的一点是, res只接受有效的json对象, 这条非常非常重要!

第一次你可能会这样写

 chrome.devtools.inspectedWindow.eval(`document.getElementsByTagName('*')`, (res)=>{
    console.log('执行结果是document: ', res)
  })

你会发现打印出了奇奇怪怪的东西如下图
image.png
image.png

考验基本功的时候到了, 你知道是为啥么? 自己思考一下

答案在这里

image.png
原来是我们获取结构的时候进行了json化处理, 那就明白了dom结构不能拿过来处理, 要在eval方法里面处理好再拿过来, 就有了如下代码.

chrome.devtools.inspectedWindow.eval(`[...document.getElementsByTagName('*')].map((item)=>item.localName)`, (res)=>{
  const obj = {}
  res.forEach((item)=>{
    obj[item] = obj[item]?(++obj[item]):1;
  })
  console.log('执行结果是: ', obj)
})

image.png
image.png

也就是把处理逻辑放在eval里面做完, 返回的是处理好的结果

十五. 拦截被审查页面请求

这个也是很常用的功能, 吧用户每个请求都分析罗列出来.


chrome.devtools.network.onRequestFinished.addListener( (res) => {
  console.log('请求体: ' , res)
});

image.png
image.png

请求的基本信息都有, 那我们能玩的花样就很多了.

十六. 为什么要把权限都配置在main文件里面

这个思想还挺有意思的, 我们平时写代码可以借鉴一下, 先把需要的权限写在配置文件里面, 这样使用你的扩展的时候可以先询问用户是否给你相关功能, 如果用户没给相关功能那么就把相关功能的对象变为空之类的, 达到了从源头配置权限的功能.

十七. 有趣的实战畅想(bug监测系统)

测试提的bug需要在bug平台上查看, 每次要切换页面, 那我可不可以与bug平台相关的api联动, 比如某一页有bug, 那么我右上角的图标就显示为红色, 并且标明bug的数量或者额类型
并且结合popup下拉后可以对bug进行基础的操作, 处理中, 已完成, 等等状态的修改.

十八. 有趣的实战畅想(页面主题修改系统)

在页面中插入dom结构, 点击更换主题的时候 , 更改本业dom结构的本省色彩系数
或者把选项写在右侧菜单的单选项里面, 让用户去选, content_script监测到缓存里面配置变了就更新页面样子.

十九. 有趣的实战畅想(监测项目内重复请求的接口)

如果项目内存在重复调用一个接口的情况下, 把这个接口指出来, 然后分析为什么会出现重复调用, 如果不是问题那就把这个接口放在白名单里不取监控.

二十. 有趣的实战畅想(vpn的代理)

既然可以拿到用户的请求信息, 那么可以用拿到的url进行请求地址的代理, 这个功能我就不在这里详细说了, 因为很多vpn的插件就是这样实现的.

end

接下来准备做3d和微前端相关的文章, 希望大家多多支持
这次就这么多, 希望和你一起进步.

查看原文

赞 21 收藏 17 评论 3

lulu_up 发布了文章 · 1月26日

记一次前端"chrome插件"基础实战分享会(建议收藏)(上篇)

记一次前端"chrome扩展"简单易懂的实战分享会(上)

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约67分钟

image

一. 为啥要学这个

身为前端工程师,浏览器是我们的主战场, 而我们不但要会使用浏览器, 还要利用浏览器, 核心是学习浏览器的一些编程思维,让我们更加厉害。
当然了看的见的好处就是我们可以利用chrome的扩展技术做一些好玩的小工具陶冶情操。
本文没有大型的概念, 目的是让你最轻松, 最快速的学会开发插件.

本文主要讲述干货主要知识,小知识点有没覆盖到的请查阅官网,如果是一个初学者理解完本篇文章也具备了开发的能力。

二. 什么样的'文件'算chrome扩展

没学习相关知识之前,以为需要是某些特定后缀的文件才是扩展程序类似我之前分享过的vscode插件vscode插件开发, 而chrome的扩展只需要一个json入口文件即可, 当然如果你要发到chrome商店的话还是需要打包一下的。
而这主要的入口文件就是manifest.json名字必须叫这个。
如下内容:

{
  "manifest_version": 2, // 使用的哪个版本的规则开发扩展
  "name": "谷歌扩展", // 这个是要显示在插件页面的
  "description": "用于分享知识", // 插件的介绍
  "version": "1.0", // 这个版本号会显示在插件下面, 所以不是随便填写的
  "browser_action": {
    "default_icon": "images/qian.png" // 默认显示的图片
  }
}

"manifest_version" 使用1的话会报错, 所以现在都用2了.

为存放相关图片, 我们建立图片文件夹
image.png

是不是不太相信, 这就完成了一个最最基本的'插件'

打开开发者模式
image.png

在控制页面导入这个插件
image.png

导入我们开发的相关文件夹, 也就是manifest.json存放的文件夹
image.png

插件已经生效了, 我们把他放到外面便于使用, 如图步骤:
image.png

现在我们就获得了属于我们自己的谷歌插件
image.png

三. 更新插件

这里更新单拿出来说, 是为了方便汇总, 看到后面大家也可以来这里查, 当然下面也会重复说相关操作

image.png

  1. 更新插件, 请点击2, 点击1也可以做到更新, 但是他是更新全部, 哪怕只有一个扩展他也会联网请求更新, 浪费半分钟的时间, 只有在电脑断网的情况下才会立刻更新本地插件, 所以你懂他的更新机制了吧, 所以开发时候推荐选用2的方法更新插件.
  2. devtools: 需要f12关闭控制台, 再f12打开控制台才可以更新, 并且不可以是chrome://extensions/, 需要选择一个有效的网址.
  3. background: 每次更新插件或者重开浏览器才刷新, 关闭某个页面是不会刷新的.
  4. popup: 每次点击都是重新加载popup程序

四. background 的概念和玩法

我们插件跑在用户的浏览器上, 那么它当然也会存在一个背后运行的后台控制系统, 要不然怎么操控这个插件?
而这个控制系统就是background, 我们在manifest.json里面加上一条

  "background":
    {
    "page": "background/background.html" // 指向我们的后台系统
  },

我们新增一个background文件夹, 里面就来编写我们的后台系统吧

注意这里可以是 "background/background.html" 也可以是"background/background.js", 差别就是后台管理页面有无dom结构的存在, 下面有例子.

如果你指向的是一个js文件, 那么系统会给我们一个默认的空的html文件引入js文件作为后台.

image.png

我们更新扩展, 就会发现扩展下面出现了查看视图
image.png

点击后你回发现他只是个控制台, 并没有什么实际的操作, 而且也没有相应的视图页面, 但是我们有console就可以开始玩耍了, 因为部分组件里面的console都是现在这里的.

五. 显示图标的玩法

悬浮显示文字

manifest.json里面"browser_action"加上这句配置
"default_title": "悬停会显示我",
image.png

这就可以为用户说明你这个图标表示的是什么用途了

显示文字

悬浮才显示那也太不直观了, 我们直接在图标上显示出文字岂不美哉?
这是我们调用的第一个api, 庆祝一下吧.
background.js

  chrome.browserAction.setBadgeText({text: '你懂'});
  chrome.browserAction.setBadgeBackgroundColor({color: 'red'});

image.png

  1. chrome这个变量是全局自带的, 直接调用也不会出错放心吧
  2. chrome.browserAction.setBadgeText 最大长度为4, 中文超过两个也显示不下了, 所以中文最多两个.
  3. background.js不可以内嵌在background.html页面里面, 会报安全权限的错误
悬浮的文字也可以设置, 那我们就做一下实时更新的效果
chrome.browserAction.setBadgeText({ text: '你懂' });
chrome.browserAction.setBadgeBackgroundColor({ color: 'red' });


let n = 1;

setInterval(() => {
    chrome.browserAction.setBadgeText({ text: 'n' + ++n });
    chrome.browserAction.setTitle({ title:'说明' + ++n });
    console.log(n);
}, 1000)

image.png

而我们的控制台, 也打印出了相应的console, 我们之后的开发调试同样要在这里进行了
image.png

可以想象, 比如你监控到页面有问题, 那么你可以用红色把错误的数量显示出来, 然后悬浮显示这个报错是说明什么, 是不是还挺方便的, 至于如何监控我们下面会讲.

六. popup的展示方式

也就是我们上面学习的小图标, 点击后出现的下拉框, 用户可以在这里进行基础的交互.

manifest.json里面"browser_action"加上这句配置
"default_popup": "popup/index.html",

新建popup文件夹, index.html文件代码为下面的

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #content {
      border: 1px solid red;
      padding: 10px;
    }
  </style>
</head>
<body>
  <button id="bt">点击更新内容</button>
  <div id="content"> 显示内容 </div>
  <script data-original="./index.js"></script>
</body>
</html>

刷新扩展我们可以点击icon查看下效果, 很奇怪哦

image.png

  1. div会独占一行, 但是他并不会撑起宽度, 我们需要自己为最外层设置宽度.
  2. position: fixed; left: 0; top: 0; 效果是奇怪的, 他会已popup本身dom为坐标系进行定位, 所以慎用!

自定义宽高后舒服多了

    body{
      width: 300px;
      height: 300px;
    }

image.png

为按钮添加点击事件

这里同样不能用内联的js, 要用外部引入的.
index.js :

const oBt = document.getElementById('bt');
const oContent = document.getElementById('content');

oBt.addEventListener('click', ()=>{
  alert('点击了按钮');
  oContent.innerHTML = "我是改变后的文字"
})

image.png
image.png

这样我们对popup内的dom就可以操作了, 但是我想让我们的图标上显示的文字 停下来我要怎么做那?

七. popup与background的通信-> extension

这里涉及到一个概念, background与popup都是独立的页面, 所以想要通讯只能用系统提供的api实现

chrome.extension.getBackgroundPage() 可以获取到Background页面的上下文的window属性.

第一步: 我在background.js里面加了实时更新icon文字的限制, 只有showTime为真才会去更新

chrome.browserAction.setBadgeText({ text: '你懂' });
chrome.browserAction.setBadgeBackgroundColor({ color: 'red' });

let n = 1;
var showTime = true;

setInterval(() => {
  if(showTime){
    chrome.browserAction.setBadgeText({ text: 'n' + ++n });
    chrome.browserAction.setTitle({ title:'说明' + ++n })
  }
}, 1000)

注意, 这里我用var定义showTime, 因为let不会挂在window上会导致访问不到这个变量.

popup/index.js文件我们获取到window对象, 并改变showTime的值

const oBt = document.getElementById('bt');
const bgWindow = chrome.extension.getBackgroundPage();

oBt.addEventListener('click', ()=>{
  // 操作 background的上下文
  bgWindow.showTime = false
  chrome.browserAction.setBadgeText({text: '占领'});
})

改变背景颜色更明显

oBt.addEventListener('click', ()=>{
  alert('点击了按钮');
  oContent.innerHTML = "我是改变后的文字";

  // 操作 background的上下文
  bgWindow.showTime = false
  chrome.browserAction.setBadgeBackgroundColor({ color: 'blue' });
  chrome.browserAction.setBadgeText({text: '占领'});

  // 异步请求
  ajaxGet('https://www.zhangzhaosong.com/', (res)=>{
    console.log('返回值', res)
  })
})

image.png

类似这种用法能不用就不用, 毕竟这种变成思想很反人类, 数据来源搞得模糊不清, 莫名其妙的错误也会纷至沓来.

八. 让我们发起请求

都是本地化的操作肯定无法满足我们, 那我们与服务端的通讯就可以玩起来了
这里出现了一个新概念权限

  "permissions": ["http://*/*", "https://*/*"]

上面的意思是容许访问网址的白名单, 我们可以根据实际情况来做出调整.

popup/index.js:

function ajaxGet(url,fn) {
    let xhr=new XMLHttpRequest();
    xhr.open("GET",url);
    xhr.send();
    xhr.onreadystatechange=function () {
        if (xhr.readyState==4&&xhr.status==200) {
            fn(xhr.responseText);
        }
    }
}

  ajaxGet('https://www.zhangzhaosong.com/', (res)=>{
    console.log('返回值', res)
  })

九. 本地储存的使用

数据不可能每次都向后台请求, 我们为了方便快捷也需要利用浏览器本地的储存方式.

manifest.json文件里面新增一条
"permissions": ["storage", "http://*/*", "https://*/*"]

了不起的chrome.storage.sync, 可以跟随当前登录用户自动同步,这台电脑修改的设置会自动同步到其它电脑;

popup/index.js中添加


let date = new Date();
const Y = date.getFullYear() + '-';
const M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
const D = date.getDate() + ' ';

// 读取的key与这个key的默认值
chrome.storage.sync.get({time: '第一次读取' }, function(items) {
    console.log(items.time);
});

chrome.storage.sync.set({time: Y+M+D}, function() {
    console.log('保存成功!');
});

在popup上点击右键, 直接开启检查
第一次读取的结果
image.png

image.png

第二次点击
image.png

这里大家应该都明白了, 用于存放用户对我们插件的一些基本配置.
比如在popup里面配置好拦截哪些请求, 处理哪些dom的样式, 设置怎样的皮肤, 然后用户下次使用的时候仍然会有这个设置数据.

下篇讲述: 坏插件如何注入的页面, 如何获取用户页面的dom, 以及有趣的想法等等内容, 图片有点多所以越写越卡, 下偏见喽!

查看原文

赞 18 收藏 14 评论 4

lulu_up 发布了文章 · 2020-12-23

记一次前端"vscode插件编写实战"超详细的分享会(建议收藏哦)(下篇)

这里的序号承接上文

十一. hover的效果(官网例子模糊)

想象一下鼠标悬停在一个语句上就会浮现出你的提示信息, 是不是还挺酷的, 说干就干,但还是忍不住吐槽一下官网的例子太粗糙了.

hover就不用去定义命令了, 因为他的触发规则就是悬停

我们新建hover.js文件

const vscode = require('vscode');
const path = require('path');

module.exports = vscode.languages.registerHoverProvider('javascript', {
  provideHover(document, position, token) {
    const fileName    = document.fileName;
    const workDir     = path.dirname(fileName);
    const word        = document.getText(document.getWordRangeAtPosition(position));
    // console.log(1, document)
    // console.log(2, position)
    // console.log(3, token)
    console.log(4, '这个就是悬停的文字', word)
    // 支持markdown语法
    return new vscode.Hover(
    `### 我就是返回的信息!
      1. 第一项:
        - 第一个元素
        - 第二个元素
      2. 第二项:
        - 第一个元素
        - 第二个元素
  `);
  }
 }
);

index.js文件如下

const vscode = require('vscode');
const message = require('./message.js');
const navigation = require('./navigation.js');
const progress = require('./progress.js');
const hover = require('./hover.js');

 function activate(context) {
    vscode.window.showInformationMessage('插件成功激活!');
    context.subscriptions.push(message);
    context.subscriptions.push(navigation);
    context.subscriptions.push(progress);
    context.subscriptions.push(hover);
}

module.exports = {
    activate
}
  1. provideHover是不用引用的
  2. 支持markdown语法
  3. 因为悬停其他插件也有相关显示, 那么vscode会按顺序展示每个插件的hover效果

image.png

由此可见, 我们hover的时候可以获得被悬停的文字, 可以获得其所在的文件位置, 那么我们就可以去node_modules里面查找对应的文件了, 然后获取到他的版本号与更新时间和官网地址, 所以那些在 package.json中hover出现详情的插件的原理, 你!懂!了!吧!

我们完全可以利用这个快捷查找一些你们公司内部的词汇的具体含义, 或者某些code与id 代表的含义.

十二. 模板的定义, 打造属于自己团队的快捷开发

``文件中配置模板

    "contributes": {
        "snippets": [
            {
                "language": "javascript",
                "path": "./snippets/cc.json" // 这里就是存放模板的文件
                }
        ]
    },

./snippets/cc.json内容如下:

{
  "名字写在这里真奇怪, 使公司模板统一": {
      "prefix": "cc 左侧名字",
      "body": [
        "for (xxxxxxxxxxxxxxxxx ${2:item} of ${1:array}) {",
        "\t$0",
        "}"
      ],
      "description": "我自己定义的cc哦, 厉害吧"
  }
}

image.png
image.png

  1. prefix就是快捷选择栏里面显示的名字, 也是你输入的字符的匹配文字
  2. body里面是我们定义的模板体
  3. ${1:array} 这里的意思是, 光标在生成模板的时候的所在位置的顺序, 当你点击tab的时候鼠标会依次出现在1,2,3这三个地方厉害吧.
有了这个功能, 与你开发的插件来个小配合, 才叫真正的快捷开发代码.

十三. 左侧标签与结构

这个就只需要两个配置对象, 并且有一张你的icon图标.

    "viewsContainers": {
        "activitybar": [
            {
                "id": "xingqiu",
                "title": "标题,并且hover显示",
                "icon": "./images/星球.png"
            }
        ]
    },
    "views": {
        "xingqiu": [
            {
                "id": "c1",
                "name": "超脱:踏天"
            },
            {
                "id": "c2",
                "name": "超脱:道无涯"
            },
            {
                "id": "c3",
                "name": "超脱:道源"
            },
            {
                "id": "c4",
                "name": "超脱:永恒"
            }
        ]
    }

image.png
image.png

  1. activitybar里面会按照id调用对应的views视图
  2. xingqiu只有一个对象则完全占领, 多个的话就是平分这里的面积
  3. 展开的时候会抢面积, 点击折叠起来就好了

十四. 树结构, 官网例子的缺乏,也没有对api进行有效的讲解(重点)

先把最终的效果图放在这里, 我们按图索骥.....
image.png

第一步: 新建tree.js文件, 在这里编写树形视图的代码.

module.exports = vscode.window.createTreeView('c1', {
    treeDataProvider: aNodeWithIdTreeDataProvider(),
    showCollapseAll: true
  });
  1. createTreeView的第一个参数是id, 也就是我们views中定义的id.

2.aNodeWithIdTreeDataProvider方法会生成一个'对象', 用来定义与生成tree的展示效果, 接下来我们会详细的讲.

  1. showCollapseAll选项全部折叠

重点讲讲aNodeWithIdTreeDataProvider
其中有三个key, 其中两个这里重点讲一下.

第一个: getChildren方法, 这里可以理解为获取数据源, 也就是树形数据, 但是他的理念是每次点击会获取一次子集数据, 比如{ a:{name:'xx'} }, 那么点击a的时候会返回{name:'xx'}这个对象, 注意:第一次执行是undefined 所以我们要在第一次执行的时候我我们定义好的树形数据传进来.

上代码:

const {c1Tree} = require('./treeData');
aNodeWithIdTreeDataProvider(){
getChildren: (el) => {
    const arr = [];
    const tree = el || c1Tree;
    for (let item in tree) {
      const activeItem = tree[item];
      if (typeof activeItem !== 'object') {
        arr.push(`${item}:${activeItem}`)
      } else {
        Object.defineProperty(activeItem, "_cc_key", {
          get: function () { return item },
          enumberable: false
        });
        arr.push(activeItem)
      }
    }
    return arr
  },
 }

./treeData.js代码如下, 这只是我当前的格式, 明白了原理你当然可以定制属于你们团队的代码啦.

const c1Tree = {
  '1': {
    '作者': "lulu",
      '宠物': {
        '名字': 'cc',
        '品种':'金毛',
        '年龄': 6,
      }
  },
  '2': {
    '作者': "lulu2",
      '宠物': {
        '名字': 'cc2',
        '品种':'金毛',
        '年龄': 9,
    }
  },
  '3': '单独的字符串'
}

module.exports = {
  c1Tree,
}
原理解析
  1. getChildren返回的是个数组
  2. getChildren返回值的每一项显示在界面上时, 都会传递给getTreeItem做处理
  3. 其实在这个函数里面我们要做的就是把数据处理成getTreeItem方便使用的形式, 这里不做具体的界面输出操作.
  4. 这里我把每个'对象数据'都赋予了_cc_key这样一个属性, 这是为了方便下面取用, 下面讲完getTreeItem我会说一下还有哪些做法.

第一个: getTreeItem方法, 每次用户点击一个下拉的项, 都会在这个方法里面获得这个项的配置.

getTreeItem: (el) => {
    let treeItem = {};
    if (typeof el === 'string') {
      treeItem = {
        label: el,
        collapsibleState: 0,
        tooltip: "hover: 单纯的字符串 ",
        // id: new Date().getTime()
      }
    } else {
      treeItem = {
        label: el._cc_key,
        collapsibleState: 1,
        tooltip: "hover: 可展开 ",
        // id: new Date().getTime()
      }
    }
    return treeItem;
  },

image.png

  1. label 界面上显示的文字
  2. collapsibleState 是否可展开, 也就是是否有子集可点击
  3. tooltip hover上时显示的提示信息
  4. id注册一个唯一标识, 这个可以不写, 如果写的话要保证id的唯一性.
  5. vscode.TreeItemCollapsibleState.None 就是不可折叠, 比直接用0更安全.
  6. vscode.TreeItemCollapsibleState.Collapsed可折叠可展开.

第三个: getParent 获取父级, 但是这次没用到这个就先不详细展开了.

完整代码如下:

const vscode = require('vscode');
const {c1Tree} = require('./treeData');

module.exports = vscode.window.createTreeView('c1', {
    treeDataProvider: aNodeWithIdTreeDataProvider(),
    showCollapseAll: true
  });
  
  function aNodeWithIdTreeDataProvider() {
    return {
      getChildren: (el) => {
        const arr = [];
        const tree = el || c1Tree;
        for (let item in tree) {
          const activeItem = tree[item];
          if (typeof activeItem !== 'object') {
            arr.push(`${item}:${activeItem}`)
          } else {
            Object.defineProperty(activeItem, "_cc_key", {
              get: function () { return item },
              enumberable: false
            });
            arr.push(activeItem)
          }
        }
        return arr
      },
      getTreeItem: (el) => {
        let treeItem = {};
        if (typeof el === 'string') {
          treeItem = {
            label: el,
            collapsibleState: 0,
            tooltip: "hover: 单纯的字符串 ",
          }
        } else {
          treeItem = {
            label: el._cc_key,
            collapsibleState: 1,
            tooltip: "hover: 可展开 ",
          }
        }
        return treeItem;
      },
      getParent: () => {
        return {}
      }
    };
  }
  
  1. 添加_cc_key属性是因为不想被for in的时候循环出来
  2. 可以用'冻结属性'的方式禁止获取
  3. 可以用属性禁止循环出来的方式禁止获取
  4. getChildren里面把字符串组装成 'key: value'的形式, 把对象添加_cc_key属性, 因为对象前面的key之后就无法取到了, 需要这里放入对象.

这样一个树形结构就完成了, 是不是整体逻辑感觉不是那么顺畅, 我也不知道vscode为啥这样设计,奇奇怪怪明明有更好的方法...

十五. 注册账号与申请token详细流程

1: 首先要有一个账号, 登录这里https://login.live.com/
需要配置一个邮箱, 地区选择中国.

2: 注册好后可以点击这里https://aka.ms/SignupAzureDevOps

3: 登录好你会看到如下图片, 输入你的项目名称与项目简介, 这里我选择public公开.
image.png

4: 点击 create project 会跳到下面的页面

5: 去配置一个token, 按图里的顺序点击
image.png
image.png
image.png
image.png
这里配置的是最宽松的条件, 因为这样才会没有那些莫名其妙的错误.

这里注意, 点击create之后可能页面没刷出来东西, 可以选择反复刷新页面, 也可以选择重新做一遍上述流程, 因为这里的token只展示一次, 之后是找不到了的, 所以千万要复制下来.

image.png

6: 这里我们就多了一个工程
image.png

十六. 发布与取消发布

vsce是“ Visual Studio代码扩展”的简称,是用于打包,发布和管理VS代码扩展的命令行工具

以后我们打包插件与发布插件都会用到这个小伙子.

npm install -g vsce
方式一. 控制台发布

打开控制台, 输入命令
后面是你登陆者的名字, 这里创建发布者
image.png
image.png
系统会依次让我们输入 名字、邮箱、刚刚获取的token
image.png
这里我改了下名字, 后面会讲这个问题

默认是登录状态的, 但是如果以后我们要切换用户的话要下面的步骤
接下来我们登陆一下 vsce login (publisher name) 登陆开发者
image.png
还需要输入一下token
image.png

问题: 重复了的话会报下面这个错, 就需要我们改一个发布者名字了
image.png

发布

vsce publish 或者 vsce publish -p xxxxxxxxtoken

如果出现报错
1:
image.png
package.json 中添加 "publisher": "发布者的名字",
如果还不行, 我们可以在官网添加这个名称进入团队
image.png

image.png

2: 他自己生成的.md文件不是很合格, 我们先清空

发布成功

image.png

两分钟左右就可以在应用商店搜到了(哈哈,下面的也是我写的demo)
image.png

自己做好的当然要安装一下试试啦
image.png
功能都在一切安好.

方式二. 网站发布

这个也是官方比较推荐的发布方式, 这里我来演示一下(创建发布者账号)
地址是:https://marketplace.visualstudio.com/manage
image.png
输入名字, id会自动相同, 这个id也是唯一的所以要起个好名字.
image.png
这里可以定义你的图标了, 并且可以把相关联的地址信息写上, 这样发布也挺方便。

在这个网站里可以直接配置发布的信息, 更新的操作如下(拖拽更新):
image.png

十七. 打包传递

如果我们直接在代码里发布是不用手动打包的, 如下情况需要手动打包。

  1. 在网站更新升级插件
  2. 插件内部使用, 采用安装包发给对应同学的方式使用

打包命令 vsce package
image.png
会出现如图vsix为后缀的文件。

这个时候我们就可以把它拖拽到网站上去更新我们的包。

想要给别人使用的话如下图:
image.png
然后会弹出文件选择框, 这样就可以实现安装包安装了。

十八. 能做什么, 要做什么。

能做什么的畅想
  1. 一个字典, 每次悬停在某个id上自动寻找对用的含义。
  2. 团队内部组件的快速生成使用代码。
  3. 工作中的各种提示, 比如定时提示你头上的bug数, 这个要让公司给你出接口了哈。
  4. 左侧栏展示今日的任务安排,等等等等的吧
要做什么

其实吧你说它功能强大也对, 但是实际上编辑器就是编辑器, 一些人想要做很多很多功能放在vscode里面,那还不如直接做个桌面端或者网站, 我们做插件因该做尽量方面的功能, 不用过多操作的功能, 更定制化的功能, 这里的优势是可以获取到开发人员当前的编辑状态, 所以一些与此无关,又略显繁琐的工作不建议放在插件里完成。

end

这次就是这样, 希望和你一起进步.

查看原文

赞 14 收藏 13 评论 2

lulu_up 回答了问题 · 2020-12-23

关于npm依赖包

自动下载的, 除非这个包的依赖没有写在配置文件里面, 那么你就要看他的文档需要你手动安装什么依赖.
1: 如果不自动帮你下载依赖, 那你是不是还要考虑他依赖的依赖的依赖?? 所以npm已处理这个问题
2: 为什么有的依赖不写在配置里, 因为有的依赖很常用, 你用的时候挂在全局就行了, 避免资源的浪费

关注 4 回答 3

lulu_up 发布了文章 · 2020-12-20

记一次前端"vscode插件编写实战"超详细的分享会(建议收藏哦)(上篇)

记一次前端"vscode插件编写实战"超详细的分享会(上)

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约 77分钟

image

一. vscode我们的战友

vscode本身都是用浏览器实现的, 使用的技术为Electron, 可以说vscode本身对我们前端工程师很友好, 在vscode上有很多优秀的插件供开发者使用, 那你有没有想过开发一款适合自己团队使用的插件?
写文章之前我在网上找了很多资料, 但视频方面的资料太少了, 官方网站也并没有被很好很完整的汉化, 文章还是有一些的但大多举的例子不好运行, 需要读者自己配置一堆东西, 官网到是提供了再github上的一套例子, 但都是用ts编写并且部分运行都有问题, 里面的思维逻辑与书写方式也不适合入门学习, 所以我才在此写一篇更加易懂的分享教程, 希望可以让刚入门的同学更好的学习起来, 话不多说我们一步步深入探索.

二. 环境的准备.

  1. node 这个不用说了吧, 前端必备
  2. git
  3. npm install -g yo generator-code 安装开发工具, 这个yo是帮助我们创建项目的, 你可以理解为cli
初始化项目

yo code 虽然很短小, 但没错就是这样的
image
这里需要我们选择初始化的工程, 这里我们主要聊插件部分, 并且ts并不是讨论的重点所以我们选择第二个JavaScript, 如果选第一个就会是使用ts开发插件.
image
输入一个插件名字, package.json中的displayName也会变成这个
image
是否也采用()内的名字, 项目名也是文件夹名, package.json中的name属性也会变成这个
image
输入对这个插件的描述, 项目名也是文件夹名, package.json中的description属性也会变成这个
image
在js文件中启动语义检测(可能用不到), jsconfig.json中compilerOptions.checkJs会变为true
image
是否初始化git仓库
image
选择钟爱的包管理方式

做完上述的步骤我们就得到了一个简易的工程

为了部分第一次做插件的同学入门, 这里暂时就不改动目录结构了, 下面介绍完配置我们在一起优化这个项目的结构.

三. 官网的第一个例子, 也许并不太适合入门时理解

extension.js 这个文件是初始项目时的入口文件, 我们的故事就是在这里展开的, 我们先把里面的注释都清理掉, 我来逐一解读一下这个基础结构.
初始化的里面有点乱, 大家可以把我下面这份代码粘贴进去, 看着舒爽多了.

const vscode = require('vscode');

function activate(context) {
    // 1: 这里执行插件被激活时的操作
    console.log('我被激活了!! 桀桀桀桀...')
    // 1: 定义了一个命令(vscode.commands)
    // 2: lulu.helloWorld 可以把它当做id
    let disposable = vscode.commands.registerCommand('lulu.helloWorld', function () {
        // 3: 触发了一个弹出框
        vscode.window.showInformationMessage('第一个demo弹出信息!');
    });
  // 4: 把这个对象放入上下文中, 使其生效
    context.subscriptions.push(disposable);
}

// 5: 插件被销毁时调用的方法, 比如可以清除一些缓存, 释放一些内存
function deactivate() {}

// 6: 忙活一大堆当然要导出
module.exports = {
    activate,
    deactivate
}
上面已经详细的标注了每一句代码, 接下来我们 按 F5 进入调试模式如图.

image

  1. 上方会出现一个操作栏, 我们接下来会经常与最后两个打交道
  2. 系统为我们新开了一个窗口, 这个窗口默认集成了我们当前开发的这个插件工程

image

下方也会出现调试台, 我们插件里面console.log打印的信息都会出现在这里.

官网这个例子需要我们 ctrl + shirt + p 调出输入框, 然后在里面输入hello w 就可以如图所示

image

用力狠狠敲击回车, 大喊神之一手, 响起bgm

image
在左下角就会出现这样一个弹出款, 这个例子也就完成了
image
组件显示被激活

那么我们一起来看一下, 这一套流程到底是怎么配置出来的!!
他的配置方式我比较不赞同, 都是在package.json里面进行的

package.json
image

activationEvents: 当什么情况下, 去激活这个插件, 也就是上面打印出桀桀怪笑.
activationEvents.onCommand: 在某个命令下激活(之后会专门列出很多其他条件)

定义命令

contributes.commands: 你可以理解为'命令'的列表
command: 命令的id (上面就是依靠这个id 找到这个命令)
title: 命令语句(可以看下图)
image
image

所以extension.js里面的registerCommand('lulu.helloWorld' 就是指定了, 这个命令Id 被执行的时候, 该执行的操作!
let disposable = vscode.commands.registerCommand('lulu.helloWorld', function () {
        // 3: 触发了一个弹出框
        vscode.window.showInformationMessage('第一个demo弹出信息!');
    });
之所以标题里说这个例子不是太好, 就是因为我们平时较少用vscode的命令去做某个操作, 并不会很生动的把我们带入进去

四. 项目结构微改 + 提示的类型

一个extension.js文件已经满足不了我们的'xie.nian'了, 所以我们要把它稍微改造一下.
老朋友src出场了, 毕竟我们以后要封装很多命令与操作, 工程化一点是必要的.

  1. 最外层新建src文件夹
  2. extension.js 改名 index.js, 放入src文件夹内.
  3. package.json中设置路径 "main": "./src/index.js"(重点入口文件)
  4. 新建“目录.md”文件, 把插件的简介完善一下, 保证任何时候最快速度理解项目。
  5. 新建message.js文件, 放置弹出信息代码.
index文件: 只负责导出引入各种功能, 不做具体的操作
const message = require('./message.js');

 function activate(context) {
    context.subscriptions.push(message);
}

module.exports = {
    activate
}
message.js 触发各种提示框
const vscode = require('vscode');

module.exports = vscode.commands.registerCommand('lulu.helloWorld', function () {
  vscode.window.showInformationMessage('第一个demo弹出信息!');
  vscode.window.showWarningMessage('第一个警告信息')
  vscode.window.showErrorMessage('第一个错误信息!');
});
目录.md(这个要随时更新不要偷懒)
# 目录

1. index 入口文件

2. message   提示信息插件

3. 

效果如下:

image.png

五. 激活的时机

出于性能的考虑, 我们的组件并不是任何时候都有效的, 也就是说不到对应的时机我们的组件处于未被激活的状态, 只有当比如说遇到js文件才会生效, 遇到scss文件才会生效, 只能某个命令才能生效等等情况.
package.jsonactivationEvents数组属性里面进行修改.
这里只是常用的类型, 具体的可以去官网文档查看.

    "activationEvents": [
        "onCommand:hello.cc",  // 执行hello命令时激活组件
        "onLanguage:javascript",  // 只有是js文件的时候激活组件(你可以做js的代码检测插件)
        "onLanguage:python", // 当时py文件的时候
        "onLanguage:json",
        "onLanguage:markdown",
        "onLanguage:typescript",
        "onDebug", // 这种生命周期的也有
        "*",
        "onStartupFinished"
    ],
  1. "*" 就是进来就激活, 因为他是任何时机(不建议使用这个).
  2. onStartupFinished他就友好多了,相当于但是他是延迟执行, 不会影响启动速度, 因为他是浏览器启动结束后调用(非要用""可以用这个代替).
这里有个坑点, 比如你有a,b 两个命令, 把a命令设为触发时机, 那么如果先执行b命令会报错, 因为你的插件还未激活.

六. 生命周期

与其他库一样, 生命周期是必不可少的(摘自官网).

七. window与mac的区别

我们知道, window与mac的案件是不大一样的, mac更多是使用command键, 这里我介绍一下分别设置快捷键.
`里面contributes`属性

"keybindings": [
 {
    "command": "hello.cc",
    "key": "ctrl+f10",
    "mac": "cmd+f10",
    "when": "editorTextFocus"
  }
],

八. when属性常用方式

接下来还有其他地方用到这个when属性那么这里就专门提一下吧
下面是比较常用的, 具体的要查官网, 毕竟太多了!

1. 编辑器获得焦点时
"when": "editorFocus"
2. 编辑器文本获得焦点
"when": "editorTextFocus"
3. 编辑器获取焦点并且四js文件的时候
"when": "editorFocus && resourceLangId == javascript"
4. 后缀不为.js
"when":"resourceExtname != .js"
5. 只在Linux,Windows环境下生效
"when": "isLinux || isWindows"
1. 要正确的理解when, 他不是字符串, 他是true 或 false的布尔, 写成字符串vscode会去解析的, 所以when可以直接传 true, false, 这里要注意, 是when: "true" when:"false"
2. 由第一条可知editorLangId其实就是运行时上下文的一个变量, 也就是文件类型名常量.

参考资料: https://code.visualstudio.com...

九. 所在的位置 左侧, 右上, 菜单的上与下

这里也只介绍最常用与好用的, 很多偏门知识学了我的这篇文章你也一定可以很轻易的自学了.
①. 鼠标右键
新建navigation.js文件用来展示我们的功能是否生效.
index.js里面引入

const vscode = require('vscode');
const message = require('./message.js');
const navigation = require('./navigation.js');

 function activate(context) {
    vscode.window.showInformationMessage('插件成功激活!');
    context.subscriptions.push(message);
    context.subscriptions.push(navigation);
}

module.exports = {
    activate
}
const vscode = require('vscode');

module.exports = vscode.commands.registerCommand('lulu.nav', function () {
  let day = new Date();
  day.setTime(day.getTime() + 24 * 60 * 60 * 1000);
  let date = day.getFullYear() + "-" + (day.getMonth() + 1) + "-" + day.getDate();
  vscode.window.showInformationMessage(`明天是: ${date}`);
});

上面是一个报告明天日期的功能, 当然我们可以在这里请求后端接口, 调取今天的人员安排列表之类的接口.

package.json里面的contributes属性中添加导航的配置.

1: 定义一个命令, 然后下面定义快捷触发这个命令

"commands": [
 {
  "command": "lulu.nav",
  "title": "点击我进行导航操作"
 }
],

2: 定义导航内容, 并且绑定点击事件

    "menus": {
     // 在编辑器的内容区域
        "editor/context": [
            {
                "when": "editorFocus", // 你懂得
                "command": "lulu.nav", // 引用命令
                "group": "navigation" // 放在导航最上方
            }
        ]
    }

image.png
image.png

②. 右上按钮
其实挺少有插件用这里的, 反而这里的市场没被占用, 想开发插件的同学可以抢先占领一下.

    "menus": {
        "editor/title": [
            {
                "when": "editorFocus", // 你懂得
                "command": "lulu.nav", // 引用命令
                "group": "navigation" // 放在导航最上方
            }
        ]
    }

image.png

③. 左侧导航栏, (这个我决定下面与tree一起讲, 因为那里是重点)

十. 加载的进度条(官网例子模糊)

加载是很常用的功能, 但是官网的例子也确实不够友好, 本着我踩过的坑就不希望别人踩的原则这里讲下进度条的实际用法.

老套路定义好命令

    {
                "command": "lulu.progress",
                "title": "显示进度条"
            }

为了方便 我们把它也定义在右键菜单里面

{
                "when": "editorFocus",
                "command": "lulu.progress",
                "group": "navigation"
            }

新建progress.js文件

const vscode = require('vscode');

module.exports = vscode.commands.registerCommand('lulu.progress', function () {
  vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: "载入xxxx的进度...",
    cancellable: true
  }, (progress) => {
    // 初始化进度
    progress.report({ increment: 0 });

    setTimeout(() => {
      progress.report({ increment: 10, message: "在努力。。。." });
    }, 1000);

    setTimeout(() => {
      progress.report({ increment: 40, message: "马上了..." });
    }, 2000);

    setTimeout(() => {
      progress.report({ increment: 50, message: "这就结束..." });
    }, 3000);

    const p = new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, 5000);
    });

    return p;
  })
});
  1. vscode.window.withProgress 第一个参数是配置, 第二个参数是操作
  2. vscode.ProgressLocation.Notification 来源信息, 让用户知道是哪个插件的进度条
  3. title 加载框标题
  4. cancellable 是否可取消
  5. progress.report 初始化进度
  6. message 拼在title后面的字符
  7. 因为返回的是promise, 所以规定调用 resolve则结束进度

image.png

进度条也算常用的基本功能, 好好用让自己的插件更友好.

可能是字数和图片有点多现在越写越卡, 只能跳到下篇去继续写啦.
查看原文

赞 35 收藏 28 评论 3

lulu_up 回答了问题 · 2020-11-30

解决vue组件中使用外部js方法

你可以这样理解, template里面调用方法相当于 this.xxx 因为他没找到this.starPhone方法

关注 5 回答 4

lulu_up 回答了问题 · 2020-11-18

vue v-for中的:style怎么使用过滤器

如果是项目内的相对地址要用require

关注 4 回答 2

lulu_up 回答了问题 · 2020-11-10

两个分支代码相差一年,git如何合并成最新代码?

单拉个项目 merge一下试试冲突多不多, 别直接在原项目上玩就行

关注 3 回答 2

lulu_up 发布了文章 · 2020-10-26

记一次前端"揭开绘制地图的神秘面纱"分享会

记一次前端"揭开绘制地图的神秘面纱"分享会

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约 70分钟

image

一. 为什么要分享前端相关的"地图"知识

  1. (大屏展示)很多公司都会有相应的大屏幕展示系统, 例如中国或者全世界的客户与资产分布图.
  2. (生动描绘)用地图的角度来展示地理方面的关系, 让人看着比单纯的文字更直观
  3. (场景多)比如今年的各类疫情严重情况的分布图.
  4. 总的来说还是看起来比较炫酷, 可以提升一点点b格, 并且这个只是也是属于前端的范畴, 那么我们就有必要弄懂它.

二. 做地图相关技术简介

这里我只介绍几款我常用的

** 百度地图
这个名气太大了, 功能很多并且现在对3d的支持也很不错, 注意GL版v1.0 与之前 v2.0版本地图的api有点不一样别掉坑里.
缺点也比较明显, 比如你想要一份干干净净的地图, 上面没有店铺没有任何标识的时候我就建议你用echarts来玩了, 因为百度地图带的东西比较多.
想要使用百度地图的同学可以看这里, 超级简单就可以完成注册用玩耍.
使用非常简单

** hcharts
非常牛非常好用, 但是它部分功能是要收费的, 使用之前要让公司帮你买好相应的功能才能用于商用哦.
由于我们公司地图库是自己研发的最后也就没有这种网上付费的.
详情地址

** echarts
这个库前端无人不知了, 在需求很简单的情况下建议用这个技术来做, 大部分时候项目中需要绘制柱状图或折线图的时候已经引入了echarts此时不用重复引用来节省空间.
echarts画的地图

** 我们公司自己的2d, 3d地图组件库
这个在这里就不做过多详细介绍了, 一些公司也会有自主研发的地图组件, 设计的思想上可能与上面三个不太相同, 接下来我也会聊到.

三.echarts实现基础地图

以echarts为例是因为这个最好弄...

这里我新建了一个vue工程

<template>
  <div class="home">
    <div id="map"></div>
  </div>
</template>

<script>
import echarts from "echarts";
import mapData from "./geo";

export default {
  name: "Home",
  data() {
    return {
      myChart: null,
    };
  },
  methods: {
    initMap() {
      this.myChart = echarts.init(document.getElementById("map"));
      echarts.registerMap("world", mapData); // 定义名称下面要用, 这样做的好处就是可以很方便的实现切换地图的效果
      this.myChart.setOption({
        series: [
          {
            type: "map",
            mapType: "world", // 自定义扩展图表类型
            label: {
              show: false,
            },
          },
        ],
      });
    },
  },
  mounted() {
    this.initMap();
  }
};
</script>
  1. 像我们平时使用echarts一样先初始化
  2. 接下来有点不同需要echarts.registerMap("world", mapData); 可以理解为把这个数据命名为'world', 方便以后的切换(这里的数据我下面会讲).
  3. 在option的配置里面设置类型是地图, 使用上面定义好的'world'类型.

效果图
image

我们可以看得出来, 地图的绘制也没什么'特殊'的, 最主要的就是那个 mapData数据, 这个数据一般叫它geojson数据, 那么接下来我们认识一下它.

四.geojson数据到底是什么

  1. geojson是用json的语法表达和存储地理数据,可以说是json的子集, 它不是专门js使用的这点要清楚.
  2. 地图上有山川, 河流, 海洋等等的地理信息, 那么如何描述一条河? 这个时候就要使用geojson格式的文件来描绘.
  3. 并不是必须用geojson, geojson只是一套规范, 各大解析器用这套规范来解析生成对应的景色, 我们完全可以制定自己的规范来实现这些, 无非是兼容性不好需要自己写绘制的解析器.

五.geojson详细介绍

英语好的可以先撸网站
1. 基本结构

{ // 可以包括点线面, 一个大的集合
  "type": "FeatureCollection", // 定义这个是个geojson文件, 这里还可以是其他值下面会说
  "features": [] // 这里放要绘制的数据
}

以后我们看到"type": "FeatureCollection"这样一行就说明这个文件是geojson规范的文件

2. 描述一个点(Feature)
地图上的打点数据

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",  // 表示这个对象是一个要素
      "properties": {}, // 这里放样式, 后面会专门说
      "geometry": { // 这里面放具体的数据
        "type": "Point",  // 专指画点
        "coordinates": [105.380859375, 31.57853542647338] // 默认是经度与纬度, 三维的话就是xyz三个值, 当然这里也不一定是经纬度(不同的坐标体系)中会讲为什么
      }
    },
  ]
}

3. 描述多个点(FeatureCollection)
**优点

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiPoint", // 多点, 也就是连续画多个同样的点
        "coordinates": [[105.380859375, 31.57853542647338],
        [105.580859375, 31.52853542647338]
        ]
      }
    },
  ]
}

4. 描述一条线(LineString)

  1. 这里还是描绘每一个点, 但这些点会连接在一起形成线
  2. 地图上的连线数据
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "LineString", // 这里所有的点会连接在一起形成线
        "coordinates": [[105.6005859375, 30.65681556429287],
        [107.95166015624999, 31.98944183792288],
        [109.3798828125, 30.031055426540206],
        [107.7978515625, 29.935895213372444]]
      }
    },
  ]
}

5. 描述多条线(MultiLineString)

  1. 这里第二组与第一组的线, 可以分隔开不会首尾相连.
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiLineString",
        "coordinates":
          [
            [
              [105.6005859375, 30.65681556429287],
              [107.95166015624999, 31.98944183792288],
              [109.3798828125, 30.031055426540206],
              [107.7978515625, 29.935895213372444]
            ],
            [
              [109.3798828125, 30.031055426540206],
              [107.1978515625, 31.235895213372444]
            ]
          ]
      }
    },
  ]
}

6. 描述一个面(Polygon, 也叫多边形)

  1. 第一个点与最后一个点要相同, 这样才能完成闭环!!
  2. 三维数组的格式需要注意
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Polygon", // 注意这里是三维数组
        "coordinates": [
          [
            [106.10595703125, 33.33970700424026],
            [106.32568359375, 32.41706632846282],
            [108.03955078125, 32.2313896627376],
            [108.25927734375, 33.15594830078649],
            [106.10595703125, 33.33970700424026]
          ]
        ]
      }
    },
  ]
}

7. 一个面里面有多个面(Polygon)

  1. 这种单一的'Polygon'里面出现多个形状, 会出现中空的情况, 类似布尔运算, 这样就可以在地图中描述那种圈型的国家
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -39.7265625,
              -3.162455530237848
            ],
            [
              127.96875,
              -3.162455530237848
            ],
            [
              127.96875,
              74.1160468394894
            ],
            [
              -39.7265625,
              74.1160468394894
            ],
            [
              -39.7265625,
              -3.162455530237848
            ]
          ],
          [
            [
              -22.5,
              15.961329081596647
            ],
            [
              110.74218749999999,
              15.961329081596647
            ],
            [
              110.74218749999999,
              70.8446726342528
            ],
            [
              -22.5,
              70.8446726342528
            ],
            [
              -22.5,
              15.961329081596647
            ]
          ]
        ]
      }
    }
  ]
}

效果如下:
image

8. 描述多个面(MultiPolygon)
优势:

  1. 写法简洁
  2. 这些点样式可以共用
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
          [
            [
              -39.7265625,
              -3.162455530237848
            ],
            [
              127.96875,
              -3.162455530237848
            ],
            [
              127.96875,
              74.1160468394894
            ],
            [
              -39.7265625,
              74.1160468394894
            ],
            [
              -39.7265625,
              -3.162455530237848
            ]
          ]
        ],
        [
          [
            [
              -22.5,
              15.961329081596647
            ],
            [
              110.74218749999999,
              15.961329081596647
            ],
            [
              110.74218749999999,
              70.8446726342528
            ],
            [
              -22.5,
              70.8446726342528
            ],
            [
              -22.5,
              15.961329081596647
            ]
          ]
        ]
        ]
      }
    }
  ]
}

这里如果重叠了就是颜色的叠加了如图所示:
image

9. 描述一个组(geometries)

  1. 比如我们为了表示一种特定的地貌那么我们可以把这个地貌数据独立起来
{
  "type": "FeatureCollection",
  "features": [
    { // 可以包括点线面, 一个独立的集合
      "type": "GeometryCollection",
      "geometries": [
        {
          "type": "Point",
          "coordinates": [108.62, 31.02819]
        }, {
          "type": "LineString",
          "coordinates": [[108.896484375, 30.1071178870],
          [108.2184375, 30.91717870],
          [109.5184375, 31.2175780]]
        }
      ]
    }
  ]
 }

10. 不同的样式(properties)

{
  "type": "FeatureCollection",
  "features": [
     {
      "type": "Feature",
      "properties": { // 专门放属性
        "stroke": "#fa9661", // 外边颜色
        "stroke-width": 4.1, // 外边宽
        "stroke-opacity": 0.7, // 外边透明度
        "fill": "#9e290c",  // 填充色
        "fill-opacity": 0.7 // 填充色透明度
      },
      "geometry": {
        "type": "Point",  // 画点
        "coordinates": [105.380859375, 31.57853542647338]
      }
    },
  ]
}

六. geojson的相关网站与工具的使用

** 展示干巴巴的数据大家看着不起劲, 这里我推荐一个绘制geojson的超棒网站地址

那么我来介绍一下如何使用这个网站高效的生成, 以及调试geojson
image

  1. 也就是最后生成的geojson, 这里的变化可以实时影响图像, 并且会有错误提示很方便编写.
  2. 绘制直线
  3. 绘制多边形也就是面, 这里注意要首位相连.
  4. 绘制矩形, 这里应该是专门封装的方法绘制矩形.
  5. 绘制点, 这里会为我们在地图上mark一下, 具体的图片需要我们自己在项目中引用.

image

  1. 点击之后进入编辑模式, 鼠标在图形上会出现小手标识, 此时可以拖动图形移动, 操作可以选择是否保留.
  2. 删除模式, 点击可以删除指定图形,操作可以选择是否保留.

image
单击图形可以出现如图所示的操作框.

  1. 添加样式属性, 上方展示的是当前样式属性
  2. 保存你的更改
  3. 删除这个图形

image

  1. 点击open可以使用本地的geojson文件进行导入绘制.
  2. save下面点击geojson可以把生成的代码文件下载到本地.

七.自制geojson解析绘制工具的思路

  1. 我们可以只做一个转换器, 也就是你随便写认为不错的格式, 最后转换成geojson的格式.
  2. 直接用你喜欢的格式来绘制图形
  3. 如果用canvas来实现就是绘制对应的图形就好了, 就是图形叠加那里需要特殊处理一下, 样式直接读取properties里面的数据进行设置.
  4. 绘制经纬度也是个问题, 毕竟在平面上不好计算经纬度(接下地图绘制章节会讲相关知识).
  5. 所以综上看来是不是绘制一张平面版的地图也没那么困难, 只要数据对了就成功一小半了.

八.地图的基本概念 (瓦片地图, 矢量地图)

** 有没有发现咱们使用的地图在放大的时候,区域都是一个方块一个方块的被加载成图像的.
** 如果你打开控制台的network还可以看到有好多png的请求.
** 地图这种超大的数据, 超多细节是如何做到快速渲染的?
** 下面是现在比较主流的两种地图的绘制模式.

栅格瓦片地图

顾名思义图片像是瓦片一样堆叠起来的格子状成为地图, 有点像拼图, 是不是感觉一点也不高大上....
但这里也是有很多问题要解决的, 比如你在俯视世界的视角看地图, 那么出现的就是世界的瓦片图片, 当高度小于一定的数值时就采用另一套相应的瓦片, 在某个高度范围内是采用放大瓦片图片的方式模拟视野的下降, 每次请求瓦片图片都需要传递: 1: 当前视口所在坐标(经纬度) 2: 当前视口宽高 3: 当前视角高度.

栅格瓦片以 256 256 或 512 512 大小的图片为介质,这种技术通常是在服务端预先将图片渲染好,前端根据地图的缩放等级,按需加载图片加以拼接,目前依旧在大规模使用,但这种方式存在一些劣势:

受到网络带宽开销和存储空间限制的影响大,离线化部署成本高,单套主题将近 500 多 G(中国)。
样式编辑完后端渲染需要时间长。
无三维的建筑数据,在 3D 场景中无高度信息。
数据保密性差。

矢量地图

顾名思义就是矢量绘制出图形, 只要不是照片肯定会小很多, 对于矢量为什么轻量并且不失真可以参考的上篇文章svg的分享svg实战

矢量瓦片采用和栅格瓦片相同的分级切割方案,不同的是,瓦片数据传输的是地理数据,包括道路、土地、建筑等,通过在前端做地图的渲染,具有如下优势:

极少占用服务器空间,降低网络开销,本地化部署只需5G空间(中国)。
地图的底图样式更换简单.
因为具有了地理数据本身,可在数据基础上做三维空间的延伸,例如 3D 建筑。
数据保密性强。

九.不同的坐标系

** 地球本身是个椭球体, 要把它以平面的方式绘制在一个矩形上也真的不好办, 现在有不少绘制的方式但是都有各自的优缺点, 感兴趣的朋友可以查查看具体的细节, 我这里就简单介绍下比较常见的方式.

  1. 经纬度EPSG:4326 也就是地图的默认坐标
    现在球体上定义好经纬度, 然后在正方形纸上画出刻度, 对应的绘制
  2. 墨卡托投影(EPSG:3785 )
    把地球放在一个圆筒里面, 假设地球内部有个光源, 那么地球在圆柱上的投影就是地图
  3. 火星坐标系

火星坐标是国家测绘局为了国家安全在原始坐标的基础上进行偏移得到的坐标,基本国内的电子地图、导航设备都是采用的这一坐标系或在这一坐标的基础上进行二次加密得到的。
火星坐标的真实名称应该是 GCJ-02 坐标,基本上所有的国内的电子地图采用的都是火星坐标系甚至 Google 地图中国部分都特意为中国政府做了偏移。

  1. 百度坐标系

火星坐标是在国际标准坐标 WGS-84 上进行的一次加密,由于国内的电子地图都要至少使用火星坐标进行一次加密,百度直接就任性一些,直接自己又研究了一套加密算法,来了个 二次加密,这就是我们所熟知的百度坐标 BD-09,当然只有百度地图使用的是百度坐标

  1. WGS-84 坐标系

GS-84 坐标是一个国际的标准,一般卫星导航,原始的 GPS 设备中的数据都是采用这一坐标系。国外的 GoogleMap、OpenStreetMap、MapBox、OpenLayer 等采用的都是这一坐标。

geojson设置坐标系
由于坐标系的不同, 那么就算绘制一个点的坐标也都不会完全相同了, 那么就需要我们来告诉使用geojson的人按哪种坐标系进行解析

{
  "type": "FeatureCollection",
  "crs": { // 定义坐标系 (如果不写就是使用经纬度的坐标系)   默认为EPSG:4326。
    "type": "name", // "type" 和 "properties"。为强制拥有
    "properties": {
      "name": "urn: ogc: def: crs: EPSG: 54013" // 这里定义具体的规则
    }
  },
  "features": [
     {},
  ]
}

使用上线的规则

{
  "type": "FeatureCollection",
  "crs": {
    "type": "link", // 这里变成了link
    "properties": {
      "href": "http://example.com/crs/42", // 这里是你设置的资源链接
      "type": "proj4" // "proj4","ogcwkt",esriwkt" 只能这三种格式
    }
  },
  "features": [
     {},
  ]
}

十.更快的前端数据 -> WebAssembly

**WebAssembly是一种新的编码方式,文件体积更小,启动速度更快,运行速度也更快,与使用JavaScript构建的Web应用相比,性能提升明显。它是多种编程语言的编译器目标,包括C++、C、Rust等。
WebAssembly 是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范。**

WebAssembly 可以明显的提升计算的速率, 还挺适合用在地图库里面的

  1. WebAssembly 和 JavaScript 结合使用, 短时间并不会替代js
  2. .wasm文件结尾的文件来标识.
  3. WebAssembly 有一套完整的语义,实际上 wasm 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率
    WebAssembly 运行在一个沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在web环境中,WebAssembly将会严格遵守同源策略以及浏览器安全策略。
    WebAssembly 设计了一个非常规整的文本格式用来、调试、测试、实验、优化、学习、教学或者编写程序。可以以这种文本格式在web页面上查看wasm模块的源码。
    WebAssembly 在 web 中被设计成无版本、特性可测试、向后兼容的。WebAssembly 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。当然,WebAssembly 不仅可以运行在浏览器上,也可以运行在非web环境下。
  4. 解析 - 解码 WebAssembly 比解析 JavaScript 要快

编译和优化 - 编译和优化所需的时间较少,因为在将文件推送到服务器之前已经进行了更多优化,JavaScript 需要为动态类型多次编译代码
重新优化 - WebAssembly 代码不需要重新优化,因为编译器有足够的信息可以在第一次运行时获得正确的代码
执行 - 执行可以更快,WebAssembly 指令更接近机器码
垃圾回收 - 目前 WebAssembly 不直接支持垃圾回收,垃圾回收都是手动控制的,所以比自动垃圾回收效率更高。目前浏览器中的 MVP(最小化可行产品) 已经很快了。在接下来的几年里,随着浏览器的发展和新功能的增加,它将在未来几年内变得更快。

说了这些都是概念, 接下来我们就一起实战一下go

十一. hello 级别的WebAssembly

中文官网
官网的实现还需要配置环境啥的搞得很正式, 入门级别其实我们更想的是尝尝鲜, 只要你会点c++就能用我接下来的方法实现.

在线生成
在线生成

image

  1. 点击转换c++代码为WebAssembly格式
  2. 点击下载转换好的文件
  3. 下载到的是个二进制文件

引用文件

    fetch("/test.wasm")
      .then((res) => res.arrayBuffer()) // 拿到Buffer格式
      .then((bytes) => WebAssembly.compile(bytes)) // 转字节码
      .then((mod) => {
        const instance = new WebAssembly.Instance(mod);
        const exp = instance.exports;
        console.log(exp._Z7showNumv())
      });
  1. exp._Z7showNumv 而不是 exp.showNum, 这个我们可以在Wat那一栏修改一下, 但是代码多了修改起来也不容易应该有禁止转换时修改名称的选项这里就不过多展开了.
  2. 注意这里会跨域, 因为属于文件协议, 你可以本地启个服务.

开发成本

  1. 需要的不只是前端技术了.
  2. bug稍微有点多, 比如不好调试, 还有的同学遇到了每次编译结果不同等问题.
  3. 社区不完善
  4. 建议这门技术先使用在封装度较高, 计算量很大的模块上.

十二.(组内篇)我写的2d与3d工程的代码介绍

这里我在组内展示一下我编写的两个项目的代码结构与遇到的问题, 就不在这里展开了毕竟涉及保密问题, 但大体思路就是把地图分成世界, 国家, 省, 市, 区 几个等级(省市区是中国的分法), 相当于一个状态机, 然后在每个状态下做相应的事比如打点与连线, 每次变换图层状态都会隐藏其他图层展示相应视野的图层.

end.

地图方面也属于前端比较有用的一环, 我今年刚接触地图相关项目也是一脸蒙, 但是详细学习了geojson等知识之后再用地图相关组件库就非常顺畅了.
这次就是这样, 希望和你一起进步.

查看原文

赞 18 收藏 14 评论 1

lulu_up 回答了问题 · 2020-10-22

element-admin 新增编辑页面怎么在同一个页面

不同的view组件装, 或者写成两个组件放在页面里v-if

关注 4 回答 4

lulu_up 发布了文章 · 2020-10-15

记一次前端SVG实战知识分享会

记一次前端SVG实战知识分享会

记录了我在组内的技术分享, 有同样需求的同学可以参考一下
分享全程下来时间大约 40分钟

image

一. svg与前端工程师

1. 作为一名前端工程师不可能不与svg打交道, 如今掌握svg的基本知识是咱们的基本技能.
2. 学svg之前最好先学一学xml的基本知识, 这样可以更好的理解文件结构.
3. svg是很不错, 但是并不是任何地方都要使用, 学习不要太刻板.

本篇重点是基础知识, 希望您看完之后可以对svg做出一些简单的修改, 或者是一个小图片不用再等ui做完给我们, 我们可以自己动手制作.

二. xml简介 (不会说的太详细)

XML 被设计用来传输和存储数据, 指可扩展标记语言(_EX_tensible _M_arkup _L_anguage)
HTML 被设计用来显示数据。

这个头部标签 <?xml version="1.0" standalone="no"?>
  1. XML标准文件头
  2. 版本号是1.0
  3. standalone代表这个xml文件是独立的还是依赖与外部dtd文件的 (作用是定义 XML 文档的合法构建模块) 类似java中的接口.
现在很多配置文件还是用xml的形式比如java代码, 这些配置文件应该是用json更好.

三. svg简介

SVG 是使用 XML 来描述二维图形和绘图程序的语言。(节选自w3school)

  • SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
  • SVG 用来定义用于网络的基于矢量的图形
  • SVG 使用 XML 格式定义图形
  • SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
  • SVG 是万维网联盟的标准
  • SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体
  1. 什么是矢量, 矢量图形: 面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点, 一个一个的图形对象, 任意的组合.

矢量图介绍短片

  1. 不失真属于老生常谈的问题了, 其实在我看来svg图片可以被我们随便修改他的样式才是最重要的, 毕竟一个png文件不好通过代码修改他的背景色, 或是某一块的大小比例.
  2. 缺点: 在window系统里, 没有图例显示不方便查看, 在mac电脑里就有个缩略图, 没找照一张图片还要把svg全打开挨个找...

<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">

  1. 定义了svg的宽高
  2. 定义了使用1.1版本svg, 就像html一样有 4与5, svg的版本也在变化.
  3. xmlns是xml namespace的缩写,也就是XML命名空间,xmlns属性可以在文档中定义一个或多个可供选择的命名空间。该属性可以放置在文档内任何元素的开始标签中。该属性的值类似于URL,它定义了一个命名空间,浏览器会将此命名空间用于该属性所在元素内的所有内容。

例如SVG< a>元素和HTML< a>如果一个被称为svg:a和另一个html:a,则可以区分该元素, 作用就是防止svg标签内的元素与html元素混乱一团.
html5中不写这句影响也不大.

兼容性如下

image

四. 基本图形

  • svg有很多基本图形如: 矩形、圆形、直线、多点成线、多边形.
  • 这些图形我们可以直接使用
<?xml version="1.0" standalone="no"?>
<!-- 1: 整体的长宽, 规则定义  -->
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- 1:矩形 -->
<!-- x, y 左上角的位置 -->
<!-- fill: 填充的颜色 -->
<rect  width="100" height="100" style="x:20; y:20;" />

<!-- 1:圆形 -->
<!-- 2: cx,cy 圆心的坐标 -->
<!-- 3: r半径 -->
<circle cx="70" cy="220" r="50" fill="red"></circle>


<!-- 3: 直线 -->
<!-- 两个点的坐标 -->
<line x1="160" y1="80" x2="300" y2="80" style="stroke:green;stroke-width:16;"></line>

<!-- 4: 多点线 -->
<!-- 会自动把第一个点与最后一个点连接起来 -->
<!-- points:一组一组的xy坐标 -->
<polyline points="500,60 330,60 420,180"></polyline>

<!-- 5: 多边形 -->
<polygon points="470,400 450,320 410,320 410,340 410,440" style="fill:red;stroke:red;stroke-width:2"></polygon>
</svg>

如图所示:

image

  1. 这里的宽高与xy的距离你可以按px理解, 但其实这个是他的比例, 使用的时候会按这个比例等比缩放.
  2. style="stroke:green;stroke-width:16;" 定义: 如果在线内定义的是 线条的颜色为绿色, 线的宽度为16
  3. style="fill:red;stroke:red;" fill指的是填充颜色, stroke边的颜色(有时候我面试的同学说经常使用svg, 但是fill属性都不知道, 场面很尴尬.)
  4. style并不是绝对的, 比如第一个矩形我也可以写成<rect width="100" height="100" x="20" y="20" />
  5. 一起其他属性: transform="rotate(30)" 旋转角度,这里不用写deg, rx:20;ry:60; 角的弧度可以做矩形的圆角.
  6. 知道了svg无非也就是个dom那我们就可以通过获取元素的形式进行对svg内部图像的修改了, 比如获取到这个svg里面的矩形setAttribute('x', 200)让他x轴变为200

五. 视野与视框

<?xml version="1.0" standalone="no"?>

<!-- 1: 视野 -->
<!-- viewBox: 我能看到哪一部分, 当前就是左上角 -->
<!-- viewBox就算小了, 但是整体的宽高是不变的, 所以会有放大缩小效果 -->
<!-- 2: 如果viewport和viewBox的宽高比不相同。你需要自己来指定如何在SVG阅读器(如浏览器)中显示SVG图像。你可以在<svg>中使用preserveAspectRatio属性来指定。 -->
<!-- preserveAspectRatio   meet就是保持原比例不失真-->
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" preserveAspectRatio="none meet">
<rect 
  width="200" height="200"
  style="x:20; y:20; rx:20;ry:60; fill:rgb(0,0,255);stroke-width:16;
stroke:black"/>

</svg>

1: 不设置viewBox
image

2: 设置后
image

viewBox不影响整体svg的大小与比例, 只是以多大的窗口展示这个svg图片

六. 样式分组(事情变得有趣了)

任何形式的代码都存在如何复用的问题, 我们不可能在画出一个不规则图形然后想再画一个一模一样的图形时, 重新画一遍
下面是复用"样式", 在g标签里面写上样式, 内部的标签会默认使用.

神奇的 g 标签

<?xml version="1.0" standalone="no"?>
<!-- 1: <g>标签分组了, 就有面向对象的概念了 -->
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- 定义了整体的属性 -->
<!-- 2: 可以多层嵌套替换 -->
<g style="fill:rgb(0,0,255);stroke-width:16">
  <rect 
    width="100" height="100"
    style="x:20; y:20; rx:20;ry:60;
  stroke:black"/>

  <rect 
    width="100" height="100"
    style="x:20; y:140; rx:20;ry:60;
  stroke:black"/>
</g>
</svg>

效果如下图, 一模一样的两个图形
image

当然<g>标签也可以嵌套使用
<?xml version="1.0" standalone="no"?>
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<g style="fill:rgb(0,0,255);stroke-width:16">
  <rect 
    width="100" height="100"
    style="x:20; y:20; rx:20;ry:60;
  stroke:black"/>

  <rect 
    width="100" height="100"
    style="x:20; y:140; rx:20;ry:60;
  stroke:black"/>
  
  <g style="fill:red;stroke-width:16">
    <rect 
      width="100" height="100"
      style="x:20; y:260; rx:20;ry:60;
    stroke:black"/>
  </g>
</g>
</svg>

如图:
image

七. 复用

我画好的图形, 我当然可以复制一份继续使用啦

使用<use>标签进行引用图形, 并且在use标签上进行新图形的操作, 可以直接设置xy之类的东西.

<?xml version="1.0" standalone="no"?>
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
  <g id="cc">
    <rect width="100" height="100" x="20" y="20" />
  </g>
 <use xlink:href="#cc" transform="translate(160,0)" fill="red"/>
</svg>

这里可能会出现作用域的问题, 但是放在html里面就没问题了
后面会讲问什么放在html里面会好, 已经如何解决这个问题!!!
有问题的同学可以用下面的代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
  <g id="cc">
    <rect width="100" height="100" x="20" y="20" />
  </g>
 <use xlink:href="#cc" transform="translate(160,0)" fill="red"/>
</svg>
</body>
</html>

效果如下:
image
image

玩到这里是不是有些.svg里面的代码在干什么已经能看懂小半了!!! 这就是会的越多越开心.

八. 渐变

<?xml version="1.0" standalone="no"?>

<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">

  <!-- 1: 定义了一组渐变, 设置id方便引用 -->
  <defs>
      <!-- 这里指定了渐变的区域, 1-0的范围 -->
      <linearGradient id="orange_red" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" style="stop-color:rgb(255,255,0);
      stop-opacity:1"/>
      <stop offset="100%" style="stop-color:rgb(255,0,0);
      stop-opacity:1"/>
      </linearGradient>
  </defs>
  <!-- 1: 引入相应的渐变颜色 -->
  <ellipse cx="200" cy="190" rx="85" ry="55"
  style="fill:url(#orange_red)"/>

</svg>
  1. <defs>里面定义了一组渐变规则, 因为xml只能所有都用标签标示, 你可以理解为这个定义了个类.
  2. <defs>标签里面定义的内容是不展示的
  3. <linearGradient> 线性渐变标签, <radialGradient>放射性渐变标签(这个原理都一样)
  4. <stop> 规定了n%的位置的颜色
  5. <ellipse> 是用来做椭圆的

如图所示:
image

九. 填充

<?xml version="1.0" standalone="no"?>

<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- 1: 定义填充类 -->
<defs>
<!-- 2: 内容写在这里 -->
<pattern id="cc" x="0" y="0" width="0.2" height="0.2">
   <circle cx="10" cy="10" r="10" fill="red">
   </circle>
</pattern>
</defs>
  <rect 
    width="400" height="400"
    fill="url(#cc)"
    style="x:20; y:20; rx:60;ry:60;stroke:black"/>
</svg>
  1. <pattern> 填充功能的标签
  2. 这里你可以理解为div的背景图案, 但是没有设置 background-repeat: no-repeat;

如图所示:
image

十. path的用法(可以一口气画的好长好长...)

<?xml version="1.0" standalone="no"?>

<svg width="600" height="600" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- 1: 把path当成一支画笔, path就是一气呵成的绘制操作 -->
<!-- 2: M 是移动的xy  m是移动的相对距离 c是curveto曲线图 -->
<!-- 3: L画直线 -->
<path d="M153 334
C153 334 151 334 151 334
C151 339 153 344 156 344
C164 344 171 339 171 334
C171 322 164 314 156 314
C142 314 131 322 131 334
C131 350 142 364 156 364
C175 364 191 350 191 334
C191 311 175 294 156 294
C131 294 111 311 111 334
C111 361 131 384 156 384
C186 384 211 361 211 334
C211 300 186 274 156 274"
style="fill:white;stroke:red;stroke-width:2"/>

</svg>
  1. <path>标签类似一气呵成的画笔, 里面会有超多绘制操作, 有了这个标签就可以很方便的封装绘制svg的编辑器了.
  2. M153 334 的意思就会在坐标为153 334的点上绘制
  • M = moveto
  • L = lineto
  • H = horizontal lineto
  • V = vertical lineto
  • C = curveto
  • S = smooth curveto
  • Q = quadratic Bézier curve
  • T = smooth quadratic Bézier curveto
  • A = elliptical Arc
  • Z = closepath

如图所示:
image

简单的也有

<?xml version="1.0" standalone="no"?>
  <svg width="600" height="600" version="1.1"
xmlns="http://www.w3.org/2000/svg">
  <path d="M150 0 L75 200 L225 200 Z" />
</svg>
  1. M150 0 绘制起始点坐标
  2. L75 200 连线到75 200
  3. L225 200 连线到255 200
  4. Z 结束绘制, 提笔.

如图所示:
image

十一. 文本输入

怎么可能少了文字的输入
<?xml version="1.0" standalone="no"?>
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- 1: 理论上svg并不是一个专业处理文字的载体 -->
<!-- 2: style标签可以使用大部分css属性啦, 不用要px -->
<!-- 3: xy对应的是左下角的基线, 而不是完美的左下角 -->
<!-- 4: 文字的颜色也要放在fill属性里面, 这也更符合工程化 -->
<text x="200" y="200" style="font-size:26;" fill="red">蚊子蚊子1</text>
<!-- 5: 针对里面每一个字符设置距离 -->
<!-- dx dy 具体到每个文字的距离 -->
<!-- 那么我其实可以巧用svg来搞点花里胡哨的 -->
<!-- 用波浪形画个❤ -->
<!-- 再加上动画效果, 就是扭动起来的文字啦 -->
<text x="200" y="300" dx="0 20 60 " dy="0 20 40" style="font-size:26;" fill="blue">蚊子蚊子2</text>

<!-- tspan标签对文字的单独处理(含镂空!!!!) -->
<text x="200" y="100" style="font-size:40">
  <tspan fill="red">蚊子3</tspan>
  <tspan stroke="blue" fill="none">蚊子4</tspan>
  </text>
</svg>
  1. <text>标签用来表示文字
  2. dx定义每个文字之间的间距
  3. <tspan> 就是<text>里面单独处理字符的标签
  4. fill="none" 就可以产生扣动字了

如图所示:
image

十二. 路径文本(让你的文字沿着路径排列而已)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>textpath</title>
</head>
<body>
<svg xmlns='http://www.w3.org/2000/svg' width='800' height='600'>
    <path id="path1" d='M 100 200 Q 200 100 300 200 T 500 200' stroke='rgb(0,255,0)' fill='none'>
    </path>
    <text style='font-size:24px;'>
        <textpath xlink:href='#path1'>
            学来学去学来, 你还学得动吗哈哈哈!!
        </textpath>
    </text>
</svg>
</body>
</html>
  1. 上面使用的path标签赋予颜色是为了让大家看清文字走向
  2. <textpath>标签需要引用一个走向(从此以后你可以画出各种线路的文字, 是不是可以秀给女朋友看??)
  3. fill='none'很重要, 不然他会变成面积图

    去掉 fill='none':
    image

    加上 fill='none:
    image

    加上这个就可以不用在html文件中才能显示了, 也就是规定了作用域

<?xml version="1.0" standalone="no"?>
<svg width="500" height="500" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 
<defs>
  <path id="my" d="M 100 200 Q 200 100 300 200 T 500 200" />
</defs>
<text x="10" y="200" style="font-size:26" fill="red">
 <textPath xlink:href="#my">
 学来学去学来, 你还学得动吗哈哈哈!!
 </textPath>  
</text>
</svg>
  1. xmlns:xlink= 属性规范了作用域
  2. 这意味着文档可访问 XLink 的属性和特性,表示前缀为xlink的元素应该由理解该规范的UA使用xlink规范来解释, 你可以理解为不与html冲突了.

十三. svg的引入(直接写svg, <embed> <object> <iframe>标签引入, 其实img也可以的 )

使用svg可不只是把svg放在html结构中

  1. 直接写在html中, 这个我上面演示过,
  2. <embed> 标签是 HTML 5 中的新标签, 标签定义嵌入的内容,也就相当于把svg结构直接插进来了。
  3. <object> 标签是 HTML 4 的标准标签,被所有较新的浏览器支持。它的缺点是不允许使用脚本, 向 HTML 代码添加一个对象, 感觉可用于服务端渲染的项目快速取得数据。
  4. iframe都快被淘汰了就别用了, 这里也不说用法了.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 1: 这个为啥网上没人说? -->
  ![](./2.基本图形.svg)
  <!-- 2: embed标签引入 -->
  <embed id="cc" data-original="./2.基本图形.svg" width="500" height="500"
  type="image/svg+xml"
  pluginspage="http://www.adobe.com/svg/viewer/install/" />
  <!-- 3: object -->
  <object data="./2.基本图形.svg" width="300" height="100" 
  type="image/svg+xml"
  codebase="http://www.adobe.com/svg/viewer/install/" />
</body>
</html>

十四. 做图工具

  1. SVG.js 官网
  2. Adobe Illustrator 官网
  3. 菜鸟工具

十五. 实际应用时如何破话svg的比例

还是要单独强调一下, svg有自己的比例, 但是如果你的项目需要把svg图片撑满容器的时候, 你就要在svg标签上设置preserveAspectRatio="none meet", 否则是不允许破话svg比例的.(也可直接preserveAspectRatio ="none")

end 结束

svg不是前端工程师的必修课, 同时也不是一门必须使用的技术, 不要听说svg好就强制使用,我们要知道它好在哪,怎么用更好才行.
但通过学习svg的相关知识可以使我们可以靠自己做出更多绚丽的东西, 也会扩充很多有趣的知识点, 铸造我们更好的思维与知识体系.
这次就是这样, 希望和你一起进步.

查看原文

赞 16 收藏 11 评论 3