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地球了, 会涉及一些数学知识, 比如三角函数你是否已经不会背了, 那我就带你研究?
     这次就是这样, 希望和你一起进步。

查看原文

赞 7 收藏 3 评论 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.

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

查看原文

赞 17 收藏 16 评论 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.

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

查看原文

赞 27 收藏 13 评论 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.

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

查看原文

赞 27 收藏 16 评论 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地球了, 那里将会很有意思。
希望与你一起进步。

查看原文

赞 53 收藏 34 评论 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

认证与成就

  • 获得 950 次点赞
  • 获得 12 枚徽章 获得 1 枚金徽章, 获得 2 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • vue-cc-ui

    使用vue编写的pc端开源ui组件库

  • cc_vue

    简易的mvc框架以及周边技术

  • cc_map_3d

    3d世界地图, 可交互性强

注册于 2019-03-03
个人主页被 16.2k 人浏览