├── .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 | --------------------------------------------------------------------------------