├── .gitignore
├── README.md
├── composer.json
├── dist_rules_example.php
└── src
├── Builder.php
└── Utils.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JKBuildHtml 基于ThinkPHP的静态站点生成器
2 |
3 | #### 介绍
4 | 完美嫁接[THINKPHP 5.0/5.1](http://www.thinkphp.cn)的静态站点生成器,可自定义生成规则,支持动态参数,支持参数的范围设置。 是一个在原来开发过程没有变化的情况下搭建静态站的解决方案。 性能方面测试有时间搞一下。 使用中有其他问题的欢迎留言。
5 | build static site, build html file for THINKPHP framework
6 |
7 | ## 适用于TP5.0和5.1的静态站点生成器
8 |
9 | 本类适用于TP5.0和5.1双版本,因为TP5.1较5.0变动比较多,所以本项目进行了版本的适配,请放心使用。
10 |
11 | ## 特点
12 |
13 | * 纯静态:生成的网站是静态htm页面,拷贝文件就是部署站点。
14 | * 方便改造:原来TP开发代码不用变动,原来TP的view文件照常写,以前怎么写模板还怎么写。
15 | * 边开发边生成html:建议封装控制器的fetch方法,边查看静态效果边开发,避免后期重新搭建后页面显示有问题,你可以盯着`public/dist`目录下的静态页面按F5刷新静态页面效果,也可以用原来的tp路径或路由查看模板效果。
16 |
17 | ## 安装
18 |
19 | ```
20 | composer require jkbuildhtml/jkbuildhtml
21 | ```
22 | ## 配置步骤需要三步
23 |
24 | 1) 新建静态规则文件`dist_rules.php`文件到application目录,静态规则见后文。
25 | 2) 添加配置参数(配置文件名 tp5是config.php tp5.1是app.php)中添加以下配置(注意斜杠不能少)
26 | ```
27 | // 静态站放置路径:
28 | 'dist_path' => 'public/',
29 | // 静态页存放文件夹名 一般放置在public下;静态站点直接指向这个目录即可:
30 | 'dist_dir_name' => 'dist',
31 | // 生成的静态页子页的存放目录,即匹配规则中没有@符号的页面的存放目录,注意例中路径中的'dist/site-pages'会进行目录匹配作为替换./或../的依据,所以这个名称在项目文件夹名中最好唯一:
32 | 'dist_sub_dir' => 'site-pages',
33 | // 要生成静态页的模块名:
34 | 'dist_module_name' => 'index',
35 | // 静态页文件名字中的参数分隔符:
36 | 'dist_file_dot' => '_',
37 | // 静态资源路径替换 静态站点根目录下会替换成 `./` 其他会替换成 `../`
38 | 'dist_src_match' => '/public/static/',
39 |
40 | ```
41 | 3) 有需要在原生tp预览模板并生成静态需求的,可以封装控制器的 fetch 方法
42 |
43 | ## 用法
44 |
45 | #### 实例化
46 | ```
47 | $builder = new \JKBuildHtml\Builder()
48 | ```
49 | #### 批量生成静态页
50 | 在任意控制器里放置一下语句即可批量生成全部静态页,你需要做的是把它放到后台的某个地方了。
51 | 页面显示是flush逐行显示的,如果想用ajax自行搞一下代码当然也行。
52 | ```
53 | $builder->buildAll();
54 | ```
55 |
56 | #### 生成单个静态页
57 |
58 | 也可以单个页面生成,一般在列表页的每行数据后面加一个 `生一个页面` 按钮:
59 | ```
60 | $builder->buildOne($path, ['id' => 5]);
61 | ```
62 | 需要注意的是单个页面生成的path 一般为控制器和方法名,必须在静态生成规则中声明,否则会提示错误。
63 |
64 | 也可以封装tp controller 的 fetch方法,这样可以边开发边生成。
65 | ```
66 | protected function fetchHtml()
67 | {
68 | $builder = new \JKBuildHtml\Builder();
69 | $builder->buildFromFetch( $html = $this->fetch(), input('get.') );
70 | return $html;
71 | }
72 |
73 | ```
74 |
75 | ## 注意事项
76 | * 所有静态资源js,css,上传文件等,必须放置在 `dist_dir_name` 配置文件夹下,静态页面会访问这些资源,如果放到这个文件夹外面,除非站点目录不是这个目录,否则访问不到。
77 | * 所有静态规则<键>全用小写
78 | * 静态规则中的<值>的路径原则是,只要能请求到的地址就可以,建议不要使用的TP路由动态参数。
79 | * 请求路径只支持GET请求
80 |
81 |
82 | ## 关于静态资源路径
83 | 本来tp的资源是放在public下任何位置的,但是有了静态生成类,那么就得按规则来
84 | 以下是建议:
85 | * 首先在public下建一个dist目录(dist的由来是写js项目的时候build的目录名,此处借用;也可改配置)
86 | * 然后把所有前端扔给你的所有静态资源文件 js,css,images放到这个文件夹下。
87 | * 把上传的文件也放dist目录里
88 | * 生成完毕后这个目录下就会生成相关的html页面
89 |
90 | ## 静态页生成规则
91 |
92 | #### 规则文件`dist_rules.php`说明:
93 |
94 | * 注:这个文件不是路由文件,和tp路由不是一回事。
95 |
96 | 键值对说明:
97 | * <键> 为生成静态页文件名:@代表dist的根目录,@index 代表首页,其他不带@的会生成在dist/site-pages;全用小写
98 | * <值> 为静态页生成模块的路径(即控制器、方法、参数),生成过程中,会直接请求这个路径。
99 |
100 | 原TP模板文件a链接路径:
101 | * 在模板里写a链接路径的时候需要按照键的规则,路径里不需要@符号
102 |
103 | ```
104 | 'index/index',
111 | '@news' => 'news/index',
112 |
113 | // 这个是带db的,表示要查询article表的id列,循环生成静态页
114 | 'news_:id' => ['news/find', 'article'],
115 |
116 | // 这个是带自定义方法的,表示要执行getjobis方法返回id为键的二维数组,循环生成静态页
117 | 'job_:id' => ['jobs/find', 'func:getjobids'],
118 |
119 | // 这个是请求tp的模块/控制器/方法,返回一个二维数组
120 | 'job_:id_:code' => ['index/index', 'func:dist/index/test'],
121 | ];
122 | ```
123 | #### <键>
124 | * 键中带:号的是有动态参数的 会生成在`dist/site-pages`目录下
125 | * 参数命名必须和db里的字段名称一致
126 | * 为防止生成错误不同参数之间需用_分开(可以修改配置)
127 |
128 | #### <值>
129 | * 值可以是一个“请求路径”,用`控制器/方法`的形式即可,请求时会自动加上自定义模块名, 如果定义了路由则写路由
130 |
131 | * 值也可以是一个数组,第一个是请求路径,会传参请求;第二个是db的名字,即参数字段所在列的所有值,系统会根据参数批量生成页面:比如'news_:id' => ['news/find', 'article'], 是查询article表里的id列,
132 |
133 | * 如果想加入db查询条件,那么就放第三个值里 比如 `id < 100`,这个会传入到db的where条件中需要符合tp查询语法, 就成了`'news_:id' => ['news/find', 'article', ['id' => ['<',100]]],` 或 `..."id < 100"]`
134 |
135 | * 如果想自定义生成id的函数,可以把第二个参数设置成一个全局的方法,可以放common.php里(函数名不用带`func:`),或任意一个控制器里 写法:`'func:admin/index/getJobIds'` 或 `'func:getjobids'`
136 |
137 | * 若采用func类型的,返回值必须是以参数为键相符的二维数组。如:`['id' => [2,3,4,5]]`
138 |
139 | * func类型可以有第三个值,作为func的参数传入
140 |
141 | #### 请求路径出现异常怎么办
142 |
143 | 静态生成控制器会直接把异常页面也生成到html文件中,不会停止生成
144 |
145 | ## 作者
146 | 冷风崔 <541720500@qq.com>
147 |
148 | ## LICENSE
149 | 完全遵循 996ICU 协议 完美开源
150 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jkbuildhtml/jkbuildhtml",
3 | "description": "Html page builder for THINKPHP framework, build static website, build html web page",
4 | "keywords" : ["Html builder for thinkphp", "build static website for thinkphp" ],
5 | "authors": [
6 | {
7 | "name": "Ryan",
8 | "email": "541720500@qq.com"
9 | }
10 | ],
11 | "version":"1.1.0",
12 | "license": "MIT",
13 | "require": {
14 | "php": ">=5.6"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "JKBuildHtml\\": "src/"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/dist_rules_example.php:
--------------------------------------------------------------------------------
1 | 'index/index',
10 | '@news' => 'news/index',
11 |
12 | // 这个是带db的,表示要查询article表的id列,循环生成静态页
13 | 'news_:id' => ['news/find', 'article'],
14 |
15 | // 这个是带自定义方法的,表示要执行getjobis方法返回id为键的二维数组,循环生成静态页
16 | 'job_:id' => ['jobs/find', 'func:getjobids'],
17 |
18 | // 这个是请求tp的模块/控制器/方法,返回一个二维数组
19 | 'job_:id_:code' => ['index/index', 'func:dist/index/test'],
20 | ];
--------------------------------------------------------------------------------
/src/Builder.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * 1,拷贝生成静态规则文件`dist_rules.php`文件到application目录
8 | * 2, 添加配置参数(配置文件名 tp5是config.php tp5.1是app.php)中添加以下配置(注意斜杠不能少)
9 | *
10 | // 静态站放置路径:
11 | 'dist_path' => 'public/',
12 | // 静态页存放文件夹名 一般放置在public下;静态站点直接指向这个目录即可:
13 | 'dist_dir_name' => 'dist',
14 | // 生成的静态页子页的存放目录,即匹配规则中没有@符号的页面的存放目录,注意例中路径中的'dist/site-pages'会进行目录匹配作为替换./或../的依据,所以这个名称在项目文件夹名中最好唯一:
15 | 'dist_sub_dir' => 'site-pages',
16 | // 要生成静态页的模块名:
17 | 'dist_module_name' => 'index',
18 | // 静态页文件名字中的参数分隔符:
19 | 'dist_file_dot' => '_',
20 | // 静态资源路径替换 静态站点根目录下会替换成 `./` 其他会替换成 `../`
21 | 'dist_src_match' => '/public/static/',
22 | *
23 | * 3, 有需要在原生tp预览模板并生成静态需求的,可以封装控制器的 fetch 方法
24 | */
25 | namespace JKBuildHtml;
26 |
27 | class Builder
28 | {
29 | protected $module_name;
30 | protected $dist_path;
31 | protected $dir_name;
32 | protected $file_dot;
33 | protected $domain;
34 | protected $src_match;
35 | protected $sub_dir;
36 | protected $tp_version;
37 |
38 | public function __construct()
39 | {
40 | $this->module_name = config('dist_module_name');
41 | $this->dist_path = config('dist_path');
42 | $this->dir_name = config('dist_dir_name');
43 | $this->sub_dir = config('dist_sub_dir');
44 | $this->file_dot = config('dist_file_dot');
45 | $this->src_match = config('dist_src_match');
46 | if( $this->module_name === null) Utils::handleException('缺少配置参数:dist_module_name');
47 | if( $this->dist_path === null) Utils::handleException('缺少配置参数:dist_path');
48 | if( $this->dir_name === null) Utils::handleException('缺少配置参数:dist_dir_name');
49 | if( $this->file_dot === null) Utils::handleException('缺少配置参数:dist_sub_dir');
50 | if( $this->sub_dir === null) Utils::handleException('缺少配置参数:dist_file_dot');
51 | if( $this->src_match === null) Utils::handleException('缺少配置参数:dist_src_match');
52 | $this->domain = request()->domain() . '/';
53 | $this->tp_version = Utils::checkTpVer();
54 | $this->dist_path = $this->tp_version == 5 ? ROOT_PATH . $this->dist_path : \think\facade\Env::get('root_path') . $this->dist_path;
55 | }
56 | /*
57 | * 生成单个html文件 参数实例:
58 | * $path : news/index
59 | * $param : ['id' => 5]
60 | * */
61 | public function buildOne($path, $params = [])
62 | {
63 | $match_filename = $this->matchFilename($path, $params);
64 | $file_name = $match_filename[0];
65 | $param_str = $match_filename[1];
66 | $keys = $match_filename[2];
67 | $file_name = $this->fetchDir($file_name);
68 | $this->buildFile(
69 | $this->module_name. '/' .$path . ( $param_str ? '?' . $param_str : ''),
70 | strtr($file_name, $keys)
71 | );
72 | }
73 | // 用于控制器封装fetch方法中 写法
74 | // $html = $this->fetch();
75 | // controller('common/JKBuildHtml')->buildFromFetch( $html, input('get.')); // 生成静态html
76 | // echo $html;
77 | public function buildFromFetch($html, $params)
78 | {
79 | $path = strtolower(request()->controller() . '/' . request()->action());
80 | $match_filename = $this->matchFilename($path, $params);
81 | $file_name = $match_filename[0];
82 | $param_str = $match_filename[1];
83 | $keys = $match_filename[2];
84 | $file_name = strtr($this->fetchDir($file_name), $keys);
85 | $file_path = $this->makeFileDir($file_name);
86 | $res = $this->filePutHtml($file_path, $html);
87 | $url = $this->module_name. '/' .$path . ( $param_str ? '?' . $param_str : '');
88 | // echo '[ 文件已生成 ] '. $url .' [ 路径 ] ' . $file_path . ' [ 长度 ] ' . $res . '
';
89 | }
90 |
91 | /*
92 | * 批量生成html
93 | * 可改成ajax非阻塞
94 | * */
95 | public function buildAll()
96 | {
97 | foreach ($this->getDistRules() as $file_name => $path) {
98 | // 不带参数的 直接请求
99 | $file_name = $this->fetchDir($file_name);
100 | // 找到参数 news_:id
101 | $expl = explode(':', $file_name);
102 | if (count($expl) > 1) {
103 | // 带参数的 拼接参数后请求
104 | // 循环获取 id 并生成 静态页文件
105 | $path_params = $this->getParams($path, $expl);
106 | $form_datas = $this->setFromData($path_params[1]);
107 | foreach ($form_datas as $form) {
108 | $this->buildFile(
109 | $this->module_name. '/' .$path_params[0] . '?' . $form['str'],
110 | strtr($file_name, $form['map'])
111 | );
112 | }
113 | } else {
114 | $this->buildFile(
115 | $this->module_name. '/' .$path,
116 | $file_name
117 | );
118 | }
119 | }
120 | echo "[ 全部生成完毕 ] 大功告成!!! ";
121 | }
122 |
123 | // 匹配静态规则中的文件名和路径,返回[生成文件名,请求地址参数后缀,替换地址中参数的数组]
124 | protected function matchFilename($path, $params)
125 | {
126 | $rules = $this->getDistRules();
127 | $param_str = '';
128 | $file_name = '';
129 | $keys = [];
130 | if(!empty($params)) {
131 | foreach ($params as $k => $p) {
132 | $param_str .= $k . '='. $p . '&';
133 | $keys[':'. $k] = $p;
134 | }
135 | $param_str = rtrim($param_str, '&');
136 | }
137 | foreach ($rules as $k=>$v) {
138 | if($path == $v)
139 | $file_name = $k;
140 | }
141 | if($file_name == '') {
142 | Utils::handleException('该路径没有设置生成静态规则');
143 | }
144 | return [
145 | $file_name,
146 | $param_str,
147 | $keys
148 | ];
149 | }
150 | protected function getDistRules()
151 | {
152 | if($this->tp_version == 5) {
153 | // tp 5.0
154 | if (!is_file(CONF_PATH . 'dist_rules.php')) {
155 | Utils::handleException('未定义生成静态页的配置文件 dist_rules.php');
156 | }
157 | $rules = include CONF_PATH . 'dist_rules.php';
158 |
159 | } else {
160 | // tp 5.1
161 | $rules = Config::get('dist_rules.');
162 | }
163 | if (!is_array($rules)) {
164 | Utils::handleException('配置文件 dist_rules.php 格式错误');
165 | }
166 | return $rules;
167 | }
168 | public function buildFile($url, $file_name)
169 | {
170 | // $url = substr($url, 0, strrpos($url, '/'));
171 | $file_path = $this->makeFileDir($file_name);
172 | $html = $this->curlRequest($url);
173 | $res = $this->filePutHtml($file_path, $html);
174 | echo '[ 文件已生成 ] '. $url .' [ 路径 ] ' . $file_path . ' [ 长度 ] ' . $res . '
';
175 | ob_flush();
176 | flush();
177 | }
178 | protected function filePutHtml($file_path, $html)
179 | {
180 | // return file_put_contents($file_path, $html);
181 | // 匹配静态资源路径 把../ 改成 ./
182 | if(strpos($file_path, $this->dir_name . '/' . $this->sub_dir .'/') !== false){
183 | $dot_line = '../';
184 | }else{
185 | $dot_line = './';
186 | }
187 | return file_put_contents($file_path, str_replace($this->src_match , $dot_line, $html));
188 | }
189 | // 全是get请求
190 | private function curlRequest($url)
191 | {
192 | $ch = curl_init();
193 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
194 | curl_setopt($ch, CURLOPT_HEADER, false); //设定是否输出页面内容
195 | curl_setopt($ch, CURLOPT_URL, $this->domain . $url);
196 | $result = curl_exec($ch);
197 | curl_close($ch);
198 | return $result;
199 | }
200 | protected function makeFileDir($file_name)
201 | {
202 | $file_path = $this->dist_path . $this->dir_name . '/' . $file_name . '.html';
203 | if (!is_dir(dirname($file_path))) {
204 | $this->createDir(dirname($file_path));
205 | }
206 | return $file_path;
207 | }
208 | // 创建文件夹
209 | private function createDir($path)
210 | {
211 | if (!file_exists($path)) {
212 | $this->createDir(dirname($path));
213 | if(mkdir($path, 0777)) echo '[ 文件夹已生成 ] '.$path . '
';
214 | else die( '[ 文件夹已生成失败 ] '.$path );
215 | }
216 | }
217 |
218 |
219 | /*
220 | * 设置路径中参数的值的范围
221 | * 比如路径中有'news/find/:id/:sn' 则$params为 ['id' => [1,2,3,4,5,6], 'sn' => ['a1','b2']] ...
222 | * 这个方法会将以上参数进行排列组合
223 | * 返回数据:
224 | array(24) {
225 | [0]=> string(15) "id=1&sn=a1"
226 | [1]=> string(15) "id=1&sn=b1"
227 | ...
228 | * */
229 | protected function setFromData($params)
230 | {
231 | $arr = $this->arrayRank($params);
232 | $res = [];
233 | foreach ($arr as $k1 => $v1) {
234 | $str = '';
235 | $map = [];
236 | foreach ($v1 as $k2 => $v2) {
237 | $str .= $k2 . '=' . rawurlencode($v2) . '&';
238 | $map[':' . $k2] = $v2;
239 | }
240 | $res[$k1] = [
241 | 'str' => rtrim($str, '&'),
242 | 'map' => $map,
243 | ];
244 | }
245 | return $res;
246 | }
247 | /*
248 | * arrayRank 数组排列组合:把多个数组里的元素进行组合排列。适用于商品规格和每个商品规格值的排列组合。
249 | */
250 | protected function arrayRank($d)
251 | {
252 | $keys = array_keys($d);
253 | if (func_num_args() > 1)
254 | $d = func_get_args();
255 | $r = array_pop($d);
256 | while ($d) {
257 | $t = [];
258 | $s = array_pop($d);
259 | if (!is_array($s))
260 | $s = [$s];
261 | foreach ($s as $x) {
262 | foreach ($r as $y) {
263 | $t[] = array_merge([$x], is_array($y) ? $y : [$y]);
264 | }
265 | }
266 | $r = $t;
267 | }
268 | foreach ($r as $k => &$v) {
269 | $v = array_combine($keys, count($v) == 1 ? [$v] : $v);
270 | }
271 | return $r;
272 | }
273 | // 获取文件夹位置
274 | protected function fetchDir($key)
275 | {
276 | if (0 === strrpos($key, '@')) {
277 | $key = substr($key, 1);
278 | } else {
279 | $key = 'pages/' . $key;
280 | }
281 | return $key;
282 | }
283 |
284 | // 返回值 第一个是请求路径,第二个是参数列表 如:['id' => [1, 2, 3], 'attr' => [ 2 , 3 ]]
285 | protected function getParams($path, $p)
286 | {
287 | unset($p[0]);
288 | foreach ($p as &$v) {
289 | $v = str_replace($this->file_dot, '', $v);
290 | }
291 | if(is_array($path)) {
292 | $temp = $path;
293 | if (0 === strrpos($temp[1], 'func:')) {
294 | $func_name = substr($temp[1], 5);
295 | if(false === strpos($func_name, '/')){
296 | $params = $func_name(isset($temp[2]) ? $temp[2] : null);
297 | } else {
298 | $params = action($func_name, isset($temp[2]) ? $temp[2] : null);
299 | }
300 | } else {
301 | $db_name = $temp[1];
302 | $where = isset($temp[2]) ? $temp[2] : '';
303 | $db = \think\Db::name($db_name)->where($where)->field(array_values($p))->select();
304 | $db_res = [];
305 | foreach ($db as $k1 => $v1) {
306 | foreach ($v1 as $k2 => $v2) {
307 | $db_res[$k2][$k1] = $v2;
308 | }
309 | }
310 | $params = $db_res;
311 | }
312 | $res = [ $temp[0], $params ];
313 | } else {
314 | $res = [ $path, null ];
315 | }
316 | return $res;
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/src/Utils.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * 1,config.php中加入以下参数:
8 | * 'dist_path' => ROOT_PATH. 'public/', // 静态站放置路径
9 | * 'dist_dir_name' => 'dist', // 静态页的文件夹名 须放置在public下
10 | * 'dist_module_name' => 'dist', // 静态页的模块名
11 | * 'dist_file_dot' => '_', // 静态页文件名字中的参数分隔符
12 | *
13 | * 2,拷贝dist_rules.php到application目录
14 | * 3, 有需要在原生tp预览模板并生成静态需求的,可以封装控制器的 fetch 方法
15 | *
16 | */
17 | namespace JKBuildHtml;
18 |
19 | use think\App;
20 |
21 | class Utils
22 | {
23 | // 检查tp版本
24 | public static function checkTpVer()
25 | {
26 | $version = 0;
27 | if(class_exists(\think\App::class) == false)
28 | {
29 | self::handleException("未发现THINKPHP框架或版本符合要求");
30 | }
31 | if (defined('THINK_VERSION')) {
32 | $version = floatval(THINK_VERSION); // 5
33 | }
34 | if (defined('\think\App::VERSION')) {
35 | $version = floatval(\think\App::VERSION); // 5.1
36 | }
37 | if ($version != 5 && $version != 5.1) {
38 | self::handleException("JKBuildHtml暂只支持THINKPHP 5.0和5.1版本");
39 | }
40 | return $version;
41 | }
42 | // 异常
43 | public static function handleException($msg)
44 | {
45 | throw new \Exception($msg);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------