0%

背景

项目里有用到inline-manifest-webpack-plugin,当把html-webpack-plugin升级到v4时,插件就报错了。原本想等插件适配,结果发现不维护了。所以自己动手实现一个吧!

webpack 构建流程

webpack的基本构建流程如下:

  1. webpack 初始化: 包括校验配置文件,初始化默认插件,创建Compiler实例等
  2. run: 执行 run 方法
  3. compilation:创建Compilation实例,回调compilation相关钩子
  4. emit:文件内容已经解析完毕,准备生成文件
  5. afterEmit:文件已经写入磁盘
  6. done:完成编译

webpack 插件实例

webpack-plugin需要实现一个apply方法,参数是Compiler实例,下面是一个简单的实例

1
2
3
4
5
6
7
8
9
class MyWebpackPlugin {
constructor(options) {}
apply(compiler) {
compiler.hooks.emit.tap("MyWebpackPlugin", (compilation) => {
console.log("MyWebpackPlugin");
});
}
}
module.exports = MyWebpackPlugin;

只需要在webpack的配置文件中,添加插件就能在控制台看见输出了

1
2
3
4
5
module.exports = {
plugins:[
new MyWebpackPlugin(),
]
};

要实现inline-manifest-webpack-plugin的功能,只需要在合适的hook里面实现我们的逻辑就行了

更多 hook

inline-manifest-webpack-plugin 的作用

将我们选择的chunk文件直接插入到html中,减少请求次数,方便做缓存等,一般用来抽离 webpack 的运行时文件(runtime.js)。它大概实现了一下三个功能:

  1. 删除index.html里的script标签
  2. 将删除标签所对应的内容直接插入index.html
  3. 删除原来的js文件

删除 index.html 里的 script 标签

inline-manifest-webpack-plugin是基于html-webpack-plugin上的插件,html-webpack-plugin为我们提供的 hook 如图所示

Events

在生成标签之前(beforeAssetTagGeneration)去掉指定 chunk 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

// 缓存第二步里插入的内容和位子
this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);
});

将删除标签所对应的内容直接插入 index.html

生成标签的时候把第一步缓存的标签插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

删除原来的 js 文件

在 webpack 生成所以文件之后,删除原本的文件

1
2
3
4
5
// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});

此时插件的功能基本上实现完成,一下是完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const sourceMappingURL = require("source-map-url");
const HtmlWebpackPlugin = require("html-webpack-plugin");

class InlineManifestWebpackPlugin {
constructor(chunkName) {
this.chunkName = chunkName || "runtime";
this.chunkScript = null;
this.chunkIndex = -1;
}

getAssetByName = (chunks, chunkName) => {
return (chunks.find((chunk) => chunk.name === chunkName) || { files: [] }).files[0];
};

apply(compiler) {
// 注入 html-webpack-plugin 的处理过程
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);

// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});
}
}

module.exports = InlineManifestWebpackPlugin;

背景

现在绝大部分异步请求都有如下类似的套路代码

1
2
3
4
loading = true;
ajax().finally(() => {
loading = false;
});

都 0202 年了,高速的网络会导致 loading 出现闪烁情况

Promise.all 的解决方案

假设我有两个 ajax 请求时间分别是50ms150ms,我现在希望不管是50ms还是150ms,loading 动画都有一个比较完整的展示时间

这种情况只需要用一个延迟的 Promise.resolve(),通过 Promise.all 方法去拉长响应时间

1
2
3
4
5
6
7
8
9
const delay = ms => new Promise((resolve, _) => setTimeout(resolve, ms));

loading = true;
Promise.all([ajaxPromise, delay(300)])
.then(handleSuccess)
.catch(handleError)
.finally(() => {
loading = false;
});

这种解决方案对于响应快的情况有点本末倒置的感觉

Promise.race 的解决方案

现在我希望响应时间超过100ms的情况才展示 loading 动画

这种情况只需要用一个延迟的 Promise.reject(),通过 Promise.race 方法去和 ajax 竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const timeout = ms =>
new Promise((_, reject) => setTimeout(() => reject(Symbol.for('timeout')), ms));

Promise.race([ajaxPromise, timeout(100)])
.then(handleSuccess)
.catch(err => {
if (Symbol.for('timeout') === err) {
loading = true;
return ajaxPromise
.then(handleSuccess)
.catch(handleError)
.finally(() => {
loading = false;
});
}
return handleError(err);
});

当我的响应时间为101ms的时候,闪烁还是无法避免的

Promise.all 和 Promise.race 的解决方案

现在我希望响应时间小于100ms时不展示 loading 动画,大于100ms时展示300ms的 loading 动画时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const timeout = ms =>
new Promise((_, reject) => setTimeout(() => reject(Symbol.for('timeout')), ms));

const delay = ms => new Promise((resolve, _) => setTimeout(resolve, ms));

const request = ({ config, target, timeoutTime = 100, delayTime = 300 }) => {
// 返回promise的ajax请求
const promise = axios(config);

const startLoading = target => {
if (!target) {
return;
}
// startLoading
};

const endLoading = () => {
// endLoading
};

const handleSuccess = data => {
// 兼容Promise.all和Promise.race不同的返回值
const response = Array.isArray(data) ? data[0] : data;
// 处理成功的情况
return Promise.resolve(response.data);
};

const handleError = ({ response }) => {
// 处理失败的情况
return Promise.reject(response);
};

return Promise.race([promise, timeout(timeoutTime)])
.then(handleSuccess)
.catch(err => {
if (Symbol.for('timeout') === err) {
startLoading(target);
return Promise.all([promise, delay(delayTime)])
.then(handleSuccess)
.catch(handleError)
.finally(() => {
endLoading();
});
}
return handleError(err);
});
};

timeoutTime 和 delayTime 可以根据自己的网站调整

什么是hook

hook可以让你在不编写class组件的情况下使用state以及其他的React特性

具体的使用方法可见官网

实现useState

我们都知道useState会返回一个状态变量和修改它的函数,就像这样

1
const [state, setState] = useState();

单一状态

那么针对一个变量的情况就简单很多了,只需要用一个全局变量就能简单的实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const state = null;

const useState = defaultState => {
if (!state) {
state = defaultState;
}
const setState = newState => {
state = newState;
};
return [state, setState()];
};

const singleState = () => {
const [count, setCount] = useState(0);
console.log(count);
setCount(count + 1);
};

多个状态

而对于多个状态可能就需要一个格外的变量num来标记,我们用的哪一 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const states = {};
let num = 0;

const useState = defaultState => {
if (!states[num]) {
states[num] = defaultState;
}
const setState = newState => {
states[num] = newState;
};
const result = [states[num], setState];
num += 1;
return result;
};

const withHook = renderFunc => {
return (...args) => {
// 重置
num = 0;
return renderFunc(...args);
};
};

withHook(function multipleState() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

console.log(count1, count2);
setCount1(count1 + 1);
setCount2(count2 + 2);
});

注意每次调用函数组件的时候应该把num重置

多个函数

对于多个函数组件的话,函数相互调用会打乱我们num的顺序,那应该怎么保持有序执行呢?这里就要用一个stack,像我们平时调试程序的时候会在浏览器控制台里面看到一个call stack调用栈,每次运行一个函数就把它入栈,执行完毕就出栈,这样就保证了顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const contextStack = [];

const useState = defaultState => {
const context = contextStack[contextStack.length - 1];
const { num, states } = context;

if (!states[num]) {
states[num] = defaultState;
}
const setState = newState => {
states[num] = newState;
};
const result = [states[num], setState];
context.num += 1;
return result;
};

const withHook = renderFunc => {
return (...args) => {
contextStack.push({ num: 0, states: [] });
const result = renderFunc(...args);
contextStack.pop();
return result;
};
};

withHook(function state1() {
const [count1, setCount1] = useState(0);

console.log(count1);
setCount1(count1 + 1);
});

withHook(function state2() {
const [count2, setCount2] = useState(0);

console.log(count2);
setCount2(count2 + 2);
});

React结合起来

React是基于Fiber来实现,对于16.6以下的版本,我们用类组件来实现

根据上面的代码,我们只需要在setState里面去更新函数就可以了,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { Component } from 'react';

const contextStack = [];

const useState = defaultState => {
const context = contextStack[contextStack.length - 1];
const { component } = context;
const states = Object.values(component.state || {});

if (component.firstRender) {
states[context.num] = defaultState;
}

const setState = num => val => {
component.setState({ [num]: val });
};

const result = [states[context.num], setState(context.num, component)];
context.num += 1;
return result;
};

const withHook = renderFunc => {
const HooksComponent = class extends Component {
constructor(props) {
super(props);
// 标记组件是否是第一次渲染,对于useState初始值的优化
this.firstRender = true;
}

componentDidMount() {
this.firstRender = false;
}

render() {
contextStack.push({ num: 0, component: this });
const result = renderFunc(this.props);
contextStack.pop();
return result;
}
};
HooksComponent.displayName = renderFunc.name;
return HooksComponent;
};

export { withHook, useState };

总结

整个实现过程还是比较容易,先从最简单的一个函数一个状态到多个函数多个状态循序渐进

实现过程参考这篇文章

什么是 gRPC

gRPC中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,可以更轻松地创建分布式应用程序和服务。gRPC基于定义服务的思想,指定可以使用其参数和返回类型远程调用的方法。在服务器端,服务器实现此接口并运行gRPC服务器来处理客户端调用。在客户端,它提供与服务器相同的方法

什么是 Protocol Buffers

Protocol Buffers是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据

Web 上的实践

准备工作

  1. 需要安装protobuf

mac 可用用 homebrew 安装,我装的版本为 3.7.1

1
brew install protobuf
  1. 安装protoc-gen-grpc-web
1
2
$ sudo mv ~/Downloads/protoc-gen-grpc-web-1.0.6-darwin-x86_64 /usr/local/bin/protoc-gen-grpc-web
$ chmod +x /usr/local/bin/protoc-gen-grpc-web
  1. 安装grpc-webgoogle-protobuf
1
2
yarn add grpc-web
yarn add google-protobuf

开始实践

  1. 定义 proto 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// proto 版本
syntax = "proto3";

// 包名
package grpc;

// 定义请求参数结构
message RequestDTO {
string id = 1;
}

// 定义返回结构
message ResponseBO {
}

// 定义请求方法
service DemoService {
// 获取用户普通话主页
rpc GetInfo (RequestDTO) returns (ResponseBO) {
}
}

proto 文件可以找后端的同事要。这里有个坑,import 路径不能像服务端的包名一样,我就只有放在一个目录里面,直接引入文件名import 'xxx.proto'

  1. 编译 js 和 client 文件
1
2
3
4
# js
protoc -I=$DIR *.proto --js_out=import_style=commonjs:$OUT_DIR
# grpc-web
protoc -I=$OUT_DIR *.proto --grpc-web_out=import_style=commonjs,mode=grpcweb:$OUT_DIR

$DIR为你 proto 文件所在路径,$OUT_DIR为输出路径,我用的当前文件. import_style采用的前端比较常用的commonjs mode支持文本和二进制,我用的grpcweb,也可以用grpctext

  1. 编写 js 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 引入路径根据你生成文件的路径做相应的调整
const { DemoServiceClient } = require('./proto/grpc/service_grpc_web_pb');
const { RequestDTO } = require('./proto/grpc/demo.js');

const client = new DemoServiceClient('https://local.hongkazhijia.com:3000');

const main = () => {
const requestDTO = new RequestDTO();
requestDTO.setId('test');

const getInfo = client.getInfo(requestDTO, {}, (err, response) => {
if (err) {
console.log(err);
} else {
console.log(response);
}
});
getInfo.on('status', status => console.log('status', status));
getInfo.on('data', data => console.log('data', data));
getInfo.on('end', end => console.log('end', end));
getInfo.on('error', error => console.log('error', error));
};

export { main };
  1. 浏览器跨域问题

开发时调用的服务端的接口和本地地址肯定会有跨域问题平时开发的时候用的 webpack-devServer 做反向代理,因为gRPC基于http2,所以在配置文件里面设置http2 = true(注意http2只支持node < 10)

DemoServiceClient 的地址指向本地,然后添加一个 proxy

1
2
3
4
5
// grcp.DemoService 为你的package名 + service名称,不知道的话先直接请求一次直接在console里面看就行了
'/grcp.DemoService': {
target: 'http://xxx.xx.xx.xx:xxxx',
changeOrigin: true,
},

至此整个流程就跑通啦,你就能在控制台看见后端返回的结果了

剩下的就是组织代码,怎么更好的集成在自己的项目中了

维基百科中的定义

在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)

而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在

1
2
3
4
5
╭─────────────╮                 ╭───────────────╮   Fire Event   ╭──────────────╮
│ │ Publish Event │ │───────────────>│ │
│ Publisher │────────────────>│ Event Channel │ │ Subscriber │
│ │ │ │<───────────────│ │
╰─────────────╯ ╰───────────────╯ Subscribe ╰──────────────╯

实现思路

  1. 作为发布者提供 subscribe 方法给订阅者

  2. 发布者提供了订阅的方法后应该将这些订阅者都存起来,记录以便日后给他们推送消息

  3. 作为发布者要有 publish 方法推送消息

  4. 订阅者如何订阅消息或者事件,订阅者还可以通过 unsubscribe 取消订阅

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class PubSub {
topics = {};

subUid = -1;

subscribe = (topic, func) => {
const token = (this.subUid += 1).toString();
// 如果没有订阅过此类消息,创建一个缓存列表
if (!this.topics[topic]) {
this.topics[topic] = [];
}
this.topics[topic].push({
token,
func,
});
return token;
};

unsubscribe = token => {
Object.values(this.topics).forEach(value => {
value.forEach((item, idx) => {
if (item.token === token) {
value.splice(idx, 1);
console.log(`unsubscribe: ${token}`);
}
});
});
};

publish = (topic, ...args) =>
this.topics[topic]?.forEach(({ func }) => {
func(args);
});
}

// 一个简单的消息记录器,记录通过我们收到的任何主题和数据
const messageLogger = msg => {
console.log(`Logging: ${msg}`);
};

const pubSub = new PubSub();

const subscription1 = pubSub.subscribe("friend1", messageLogger);
const subscription2 = pubSub.subscribe("friend2", messageLogger);

pubSub.publish("friend1", "hello, friend1!");
pubSub.publish("friend2", "hello, friend2!");

pubSub.unsubscribe(subscription1);

pubSub.publish("friend1", "goodbye, friend1!");
pubSub.publish("friend2", "goodbye, friend2!");

// console
// Logging: hello, friend1!
// Logging: hello, friend2!
// unsubscribe: 0
// Logging: goodbye, friend2!

背景

公司最近要做一个 web 网页(只设计了 pc 端)移动端的适配,我也是第一次做涉及到移动端的开发,前期在网上查了一些资料,总结方案如下:

  1. 固定高度,宽度自适应

  2. 固定宽度,viewport 缩放

  3. rem 做宽度,viewport 缩放

用 rem 来页面,会根据不同的设备宽度在根元素上设置不同的字体大小。然后使用px替换rem。这样做以后,字体大小,内容尺寸,对随着屏幕宽度的变大而变大。

第一种方案太笨,后面两种都可以做到更好的适配,第三种比较灵活一点。所以采用第三种方案来做适配

涉及到的知识点

  1. viewport

    在电脑浏览器中,viewport 就是浏览器窗口的宽度高度。在移动端设备上就很复杂,简单点来说就是你看到的屏幕大小

  2. 物理像素

    物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件

  3. 设备独立像素(density-independent pixel)

    设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说 CSS 像素),然后由相关系统转换为物理像素

  4. CSS 像素

    CSS 像素是一个抽像的单位,主要使用在浏览器上,用来精确度量 Web 页面上的内容。一般情况之下,CSS 像素称为与设备无关的像素(device-independent pixel),简称 DIPs

  5. 屏幕密度

    屏幕密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)

  6. 设备像素比(device pixel ratio)

    简称 dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按设备像素比 = 物理像素 / 设备独立像素计算得到

  7. rem

    就是根元素<html>font-size大小

实践

使用lib-flexible,它会帮我们计算好 rem。接下来把视觉稿中的px转换成rem就行了,但是每次转换会很费时间,所以我们可以借助插件来帮我们完成,这里我采用的是 px2rem-loader(webpack)。最后你就可以放心的按照设计稿开发了。


参考使用 Flexible 实现手淘 H5 页面的终端适配

实现原理

vuex 或者 mobx 都是通过 Object.defineProperty 来实现数据劫持。当我们访问或设置对象的属性的时候,都会触发相对应的函数,通过劫持对象属性的 setter 和 getter 操作,来进行双向绑定。其主要由下面三个部分组成:

  • Observer 负责数据劫持,把所有的属性定义成被观察者,达到对数据进行观测的目的

  • Watcher 数据的观察者,在数据发生变化之后执行的相应的回调函数

  • Dep 每一个 observer 会创建一个 Dep 实例,实例在 get 数据的时候为数据收集 watcher,在 set 的时候执行 watcher 内的回调方法

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Dep
class Dep {
sub = [];
addSub = watcher => this.sub.push(watcher);
notify = () => this.sub.forEach(watcher => watcher.fn());
}

// Observer
class Observer {
constructor(obj, key, value) {
const dep = new Dep();
let backup = value;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
// 存储依赖
dep.addSub(Dep.target);
}
return backup;
},
set(newVal) {
if (backup === newVal) return;
backup = newVal;
// 执行依赖
dep.notify();
},
});
}
}

// Watcher
class Watcher {
constructor(data, k, fn) {
this.fn = fn;
Dep.target = this;
// 触发get方法
data[k];
Dep.target = null;
}
}

const observable = obj => {
if (Object.prototype.toString.call(obj) === "[object Object]") {
// 递归
Object.entries(obj).forEach(([key, value]) => {
if (Object.prototype.toString.call(value) === "[object Object]") {
observable(value);
}
new Observer(obj, key, value);
new Watcher(obj, key, (v, key) => {
console.log("你修改了数据");
// ...你想要的操作
});
});
}
};

下载

Pre-built

集成

将下载好的包放你项目的里,nginx 配置

1
2
3
4
location / {
# 没找到对应html返回index.html
try_files $uri /index.html =404;
}

访问

根据你的路径访问 web/viewer.html?file=${yourpdf}

安装 Geoserver

下载geoserver并解压

1
2
cd /geoserver/bin/
./startup.bat

访问geoServer 初始账号:admin 密码:geoserver

geoServer 默认端口为 8080,其值对应文件 start.ini 里的 jetty.port,可修改

Geoserver 数据源

PostGis

ubuntu 16.04 安装教程

Postgresql 设置默认用户 postgres 的密码

1
2
3
4
5
6
7
8
// 登录到 postgresql 窗口
sudo -u postgres psql postgres

// 用下面的命令为用户 postgres 设置密码:
postgres=# \password postgres
Enter new password:
Enter it again:
postgres=# \q

Postgresql 开启远程链接

1
2
3
4
// 找到pg_hba.conf文件,加入
host all all 0.0.0.0/0 trust
// 重启postgresql
sudo service postgresql reload

安装 osm2pgsql

1
2
3
4
5
cd ~
git clone git://github.com/openstreetmap/osm2pgsql.git
cmake ..
make
sudo make install

也可以参考 github 上面的步骤,少什么依赖就装什么

导入 osm 数据(不包含 openstreetmap-carto 样式)

下载OpenStreetMap

为 postgresql 安装以下扩展

1
2
3
4
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
CREATE EXTENSION ogr_fdw;
CREATE EXTENSION hstore;

正式导入数据

1
osm2pgsql -s -U postgres -H 127.0.0.1 -P 5432 -W -d chinaosmgisdb /tmp/china-latest.osm.pbf

用户,数据库,文件地址按实际情况变动即可

导入 openstreetmap-carto 样式

下载openstreetmap-carto

正式导入数据

1
2
3
su postgres

osm2pgsql -s -U postgres -H 127.0.0.1 -P 5432 -W -d chinaosmgisdb /tmp/china-latest.osm.pbf --style /home/hldev/openstreetmap-carto-master/openstreetmap-carto.style

shapefile

curl -v -u admin:@pp$4boundleSS -XPOST -d@layergroup.xml -H “Content-type: text/xml” http://apps.opengeo.org/geoserver/rest/workspaces/osm/layergroups

GeoServer 创建图层

创建图层数据表

下载osmsld.zip

1
2
3
4
5
6
7
wget -O osmsld.zip http://files.cnblogs.com/files/think8848/osmsld.zip

unzip osmsld.zip

su postgres

psql -U think8848 -W -d chinaosmgisdb -a -f /tmp/osmsld/create_tables.sql

在 GeoServer 中创建工作空间和数据源(postgis)

创建样式和图表

将之前我们下载的 osmsld.zip 文件中的 sld.zip 解压开 unzip sld.zip ,然后稍修改下 SLD_create.sh 文件,主要是修改 GeoServer 的 REST API 相关参数

在本文件的最下面,将最后两行暂不用的命令注释掉

然后进入刚才解压 sld.zip 形成的 sld 目录 cd sld ,然后调用以下命令

sudo sh /tmp/osmsld/SLD_create.sh

创建图层组

打开 osmsld.zip 包中的 layergroup.xml 文件,将 ocean 这一节给删掉,因为并没有导入海图数据

打开 SLD_create.sh,参照刚刚注释掉的 2 行命令创建图层组

1
curl -v -u admin:geoserver -XPOST -d@layergroup.xml -H "Content-type: text/xml" http://localhost:8080/geoserver/rest/workspaces/china/layergroups

地图乱码问题

安装一个支持中文的字体,如微软雅黑

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建字体目录,并且将msyh.ttf和msyhbd.ttf复制到字体目录中
sudo mkdir -p /usr/share/fonts/win

sudo mv msyh.ttf msyhbd.ttf /usr/share/fonts/win

// 建立字体索引信息,更新字体缓存
cd /usr/share/fonts/win

sudo mkfontscale

sudo mkfontdir

fc-cache

然后把 style 里面的字体替换

导入 OpenStreetMap 海图数据,并在 GeoServer 上发布

下载OpenStreetMap 海图数据

因为下载的 OpenStreetMap 的中国数据是 Mercator 投影坐标系,SRID 为 3857,s 所以下载第二个

将 shp 文件导入到 PostGis 中

1
2
3
su postgres

shp2pgsql -s 3857 -I -D /tmp/water-polygons-split-3857/water_polygons.shp ocean_all | psql -d chinaosmgisdb -U postgres

将发布的地图范围所在区域(bounds)海图从完整的海图中切出来

1
2
3
4
5
6
7
8
9
10
psql -U postgres -d chinaosmgisdb -W

CREATE TABLE ocean AS
WITH bounds AS (
SELECT ST_SetSRID(ST_Extent(way)::geometry,3857) AS geom
FROM planet_osm_line
)
SELECT 1 AS id, ST_Intersection(b.geom, o.geom) AS geom
FROM bounds b, ocean_all o
WHERE ST_Intersects(b.geom, o.geom);

在 GeoServer 中创建 ocean 图层

直接在 GeoServer 建图层即可,唯一要注意的就是在图层样式中选择 chinaosm:ocean 样式

Openlayers

Openlayers 比较简单,主要难点在于对 API 的熟练度。官网提供了大量例子来参考


参考think8848 的博客

彻底删除 nginx(Ubuntu)

罗列出与 nginx 相关的软件

1
2
dpkg --get- selections|grep nginx
sudo apt-get remove ... //删除

nginx 配置 http2 模块(待完成)