├── src ├── util │ ├── index.js │ ├── request.js │ └── service.js ├── assets │ ├── less │ │ ├── var.less │ │ └── common.less │ └── font │ │ └── iconfont.less ├── views │ ├── bar.vue │ └── foo.vue ├── components │ └── Hello.vue ├── server │ ├── process.js │ └── index.js ├── main.js ├── router │ └── index.js └── app.vue ├── .gitignore ├── public ├── favicon.ico └── index.php ├── themeConfig.js ├── webpack ├── webpack.dev.js ├── webpack.common.js └── webpack.prod.js ├── package.json └── readme.md /src/util/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/assets/less/var.less: -------------------------------------------------------------------------------- 1 | // 定义less变量 2 | @border: #ccc; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/17px/typecho-theme-builder/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/views/bar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/views/foo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/less/common.less: -------------------------------------------------------------------------------- 1 | // 全局css 2 | @import "./var.less"; 3 | @import "../font/iconfont.less"; 4 | 5 | body { 6 | font-size: 14px; 7 | line-height: 1.5; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /src/server/process.js: -------------------------------------------------------------------------------- 1 | // 接口数据清洗 2 | export default { 3 | getPages: async d => { 4 | let pagesList = []; 5 | for (let item of d) if (item) pagesList.push(item); 6 | return pagesList; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import Request from "../util/request"; 2 | import Process from "./process"; 3 | 4 | /** 5 | * 获取全部页面 6 | */ 7 | export const getPages = () => { 8 | return Request("/api/pages").then(res => Process.getPages(res.data.dataSet)); 9 | }; 10 | -------------------------------------------------------------------------------- /themeConfig.js: -------------------------------------------------------------------------------- 1 | // 主题配置 2 | module.exports = { 3 | // 主题名称 4 | name: "Yuna", 5 | // 打包上线的版本管理 6 | ver: "1.0", 7 | server: { 8 | // 后端Restful插件接口基础地址,如果开启伪静态,修改为'/' 9 | url: "/typecho/index.php", 10 | // axios请求超时时间 11 | timeout: 8000 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/util/request.js: -------------------------------------------------------------------------------- 1 | import Service from "./service"; 2 | 3 | const request = (api = "", params = {}, type = "get") => { 4 | let data = type === "get" ? "params" : "data"; 5 | return Service({ 6 | url: api, 7 | [data]: params, 8 | method: type 9 | }).then(res => res.data); 10 | }; 11 | 12 | export default request; 13 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./app.vue"; 3 | import router from "./router"; 4 | import "./assets/less/common.less"; 5 | 6 | router.beforeEach((to, from, next) => { 7 | /* 路由发生变化修改页面title */ 8 | if (to.meta.title) { 9 | document.title = to.meta.title; 10 | } 11 | next(); 12 | }); 13 | 14 | new Vue({ 15 | router, 16 | render: h => h(App) 17 | }).$mount("#app"); 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | 4 | const Foo = { template: "
foo
" }; 5 | const Bar = { template: "
bar
" }; 6 | 7 | Vue.use(VueRouter); 8 | 9 | const routes = [ 10 | { 11 | path: "/foo", 12 | component: Foo, 13 | meta: { 14 | title: "Foo Page" 15 | } 16 | }, 17 | { 18 | path: "/bar", 19 | component: Bar, 20 | meta: { 21 | title: "Bar Page" 22 | } 23 | } 24 | ]; 25 | 26 | const router = new VueRouter({ 27 | routes 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /src/util/service.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import THEME from "../../themeConfig"; 3 | 4 | const service = axios.create({ 5 | baseURL: THEME.server.url, 6 | timeout: THEME.server.timeout 7 | }); 8 | 9 | // 请求 10 | service.interceptors.request.use( 11 | config => { 12 | // reqNum++; 13 | // loading.show(); 14 | return config; 15 | }, 16 | error => Promise.reject(error) 17 | ); 18 | 19 | // 响应 20 | service.interceptors.response.use( 21 | response => { 22 | // reqNum--; 23 | // if (reqNum <= 0) { 24 | // loading.hidden(); 25 | // } else { 26 | // loading.show(); 27 | // } 28 | return response; 29 | }, 30 | error => Promise.reject(error) 31 | ); 32 | 33 | export default service; 34 | -------------------------------------------------------------------------------- /src/assets/font/iconfont.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* project id 1554341 */ 3 | src: url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.eot"); 4 | src: url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.eot?#iefix") 5 | format("embedded-opentype"), 6 | url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.woff2") format("woff2"), 7 | url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.woff") format("woff"), 8 | url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.ttf") format("truetype"), 9 | url("//at.alicdn.com/t/font_1554341_j7v6ta5z5a.svg#iconfont") format("svg"); 10 | } 11 | 12 | .iconfont { 13 | font-family: "iconfont" !important; 14 | font-size: 16px; 15 | font-style: normal; 16 | -webkit-font-smoothing: antialiased; 17 | -webkit-text-stroke-width: 0.2px; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | const path = require("path"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | 6 | // 载入主题配置 7 | const THEME = require("../themeConfig"); 8 | 9 | module.exports = merge(common, { 10 | mode: "development", 11 | watch: true, 12 | output: { 13 | path: path.resolve(__dirname, `../../${THEME.name}`), 14 | filename: `${THEME.name}.js` 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(css|less)$/, 20 | use: ["style-loader", "css-loader", "less-loader"] 21 | } 22 | ] 23 | }, 24 | plugins: [ 25 | new HtmlWebpackPlugin({ 26 | filename: "index.php", 27 | template: path.join(__dirname, "../public/index.php"), 28 | inject: false, 29 | favicon: path.join(__dirname, `../public/favicon.ico`), 30 | hash: true 31 | }) 32 | ] 33 | }); 34 | -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { VueLoaderPlugin } = require("vue-loader"); 3 | 4 | // 载入主题配置 5 | const THEME = require("../themeConfig"); 6 | 7 | // 公共webpack配置 8 | module.exports = { 9 | stats: { 10 | modules: false, 11 | children: false, 12 | chunks: false, 13 | chunkModules: false 14 | }, 15 | entry: path.join(__dirname, "../src/main.js"), 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.vue$/, 20 | loader: "vue-loader" 21 | }, 22 | { 23 | test: /\.js$/, 24 | loader: "babel-loader" 25 | }, 26 | { 27 | test: /\.(png|jpg|gif|jpeg)$/, 28 | use: [ 29 | { 30 | loader: "url-loader", 31 | options: { 32 | esModule: false, 33 | limit: true 34 | } 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | plugins: [new VueLoaderPlugin()], 41 | // cdn 42 | externals: { 43 | vue: "Vue", 44 | "vue-router": "VueRouter" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | 40 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | hello-world 21 | 22 | 23 | 24 | 25 | 26 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yuna", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --config ./webpack/webpack.dev.js", 8 | "build": "webpack --config ./webpack/webpack.prod.js" 9 | }, 10 | "author": "陈睿", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@babel/core": "^7.7.7", 14 | "axios": "^0.19.0", 15 | "babel-loader": "^8.0.6", 16 | "clean-webpack-plugin": "^3.0.0", 17 | "css-loader": "^3.4.0", 18 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 19 | "html-webpack-plugin": "^3.2.0", 20 | "image-webpack-loader": "^6.0.0", 21 | "less": "^3.10.3", 22 | "less-loader": "^5.0.0", 23 | "mini-css-extract-plugin": "^0.9.0", 24 | "optimize-css-assets-webpack-plugin": "^5.0.3", 25 | "style-loader": "^1.1.2", 26 | "terser-webpack-plugin": "^2.3.1", 27 | "url-loader": "^3.0.0", 28 | "vue": "^2.6.11", 29 | "vue-html-loader": "^1.2.4", 30 | "vue-loader": "^15.8.3", 31 | "vue-router": "^3.1.3", 32 | "vue-style-loader": "^4.1.2", 33 | "webpack": "^4.41.4", 34 | "webpack-merge": "^4.2.2" 35 | }, 36 | "devDependencies": { 37 | "vue-template-compiler": "^2.6.11", 38 | "webpack-cli": "^3.3.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | const path = require("path"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 6 | const TerserJSPlugin = require("terser-webpack-plugin"); 7 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 8 | 9 | // 载入主题配置 10 | const THEME = require("../themeConfig"); 11 | 12 | module.exports = merge(common, { 13 | mode: "production", 14 | output: { 15 | path: path.resolve(__dirname, `../../${THEME.name}/${THEME.ver}/`), 16 | filename: `${THEME.name}.${THEME.ver}.min.js` 17 | }, 18 | optimization: { 19 | minimizer: [ 20 | new TerserJSPlugin({}), 21 | new OptimizeCssAssetsPlugin({}) 22 | ], 23 | minimize: true 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(css|less)$/, 29 | use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"] 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new MiniCssExtractPlugin({ 35 | filename: `${THEME.name}.${THEME.ver}.min.css`, 36 | hash: true 37 | }), 38 | new HtmlWebpackPlugin({ 39 | filename: "index.php", 40 | template: path.join(__dirname, "../public/index.php"), 41 | minify: { 42 | removeComments: true, 43 | collapseWhitespace: true, 44 | minifyCSS: true 45 | }, 46 | favicon: path.join(__dirname, `../public/favicon.ico`), 47 | inject: false, 48 | hash: true 49 | }) 50 | ] 51 | }); 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] 2 | 3 | > **警告**: 该项目已经废弃,不再维护或更新。我们建议使用 [替代项目或工具的名称] 作为替代方案。感谢过去所有对该项目的支持与贡献。 4 | 5 | 6 | # Yuna - typecho 主题开发脚手架 7 | 8 | 在`typecho`开发的三个月时间里,经过一些摸索实践,最终决定创造了`Yuna`,一个`typecho`主题开发脚手架,基于`vue`、`webpack`构建,对`typecho`主题由`ssr`服务器端渲染,改造成了`spa`应用,从而使得`typecho`主题开发、迭代版本、功能时,变得更加得心应手。 9 | 10 | 非常感谢`Moe`大佬、团队制作了`Restful`风格接口插件,让这个构建方案得以实现 11 | [moefront/typecho-plugin-Restful](https://github.com/moefront/typecho-plugin-Restful) 12 | 13 | ## 快速开始 14 | 15 | - 确保已经安装了`node`环境以及`npm`包管理工具 16 | 17 | - 使用`git clone`命令、打包下载,将项目克移动到`typecho/usr/theme`目录中,执行`npm install` 18 | 19 | - 下载并安装 [moefront/typecho-plugin-Restful](https://github.com/moefront/typecho-plugin-Restful) 插件,开启`typecho`后端`Restful`接口 20 | 21 | 22 | > 如果依赖安装速度非常慢,推荐国内开发者为`npm`配置淘宝镜像、`cnpm`、当然`yarn`也是非常好的选择 23 | 24 | ## 结构 25 | 26 | 这是一个相对基础的`vue`、`webpack`构建主题开发前端方案,你可以在这基础之上,继续定制你需要的额外功能,比如替换`less`使用`sass`、`stylus`等,需要具备简单的`webpack`使用能力 27 | 28 | ``` 29 | ├─public 30 | │ ├─favicon.ico # 网站的favicon 31 | │ └─index.php # typecho首页 32 | ├─src 33 | │ ├─assets 34 | │ │ ├─font # css字体,推荐使用iconfont 35 | │ │ └─less # css样式文件,别问,less天下第一 36 | │ ├─components # 存放vue组件 37 | │ ├─router 38 | │ │ ├─index.js # vue-router路由配置文件 39 | │ ├─server 40 | │ │ ├─index.js # typecho后端api 41 | │ │ └─process.js # 接口数据清洗 42 | │ ├─util 43 | │ │ ├─index.js # 一些工具方法 44 | │ │ ├─request.js # typecho后端api请求方法封装 45 | │ │ └─service.js # axios的全局拦截、封装 46 | │ ├─views # 存放视图模板 47 | │ ├─main.js # vue.js入口文件 48 | │ └─app.vue # 视图入口 49 | └─webpack 50 | ``` 51 | 52 | ## 开发、生产环境 53 | 54 | 脚手架通过`process.ENV`区分了`webpack`的开发环境、生产环境的配置,运行`npm run dev`之后,就可以专注主题开发了,只需要向开发`vue`项目那样,只不过由于`typecho`运行在本地的`apache`、`nginx`服务中,所以当代码发生变动,需要手动刷新浏览器才能看到变化 55 | 56 | - 开发环境:当运行`npm run dev`时,`webpack`会在`typecho/usr/theme`文件夹中生成你的主题文件夹,并通过`loader`注入相关的`css、js`资源到`index.php`中 57 | 58 | - 生产环境:当运行`npm run build`时,`webpack`会根据`themeConfig.js > ver`属性的`版本号`生产版本文件夹,并且将静态资源进行打包、压缩,注入到`index.php`中 59 | 60 | ```bash 61 | # 在生产环境中创造主题,运行 62 | npm run dev 63 | # 打包主题上线,运行 64 | npm run build 65 | ``` 66 | 67 | > 别忘了后台启用主题 68 | 69 | ## 版本管理 70 | 71 | `themeConfig.js`中的`ver`属性,可以帮助你在创造不同版本的主题的时候,进行版本的管理,默认为`1.0`,如果你打算进行`1.0.1`版本的开发,那么就修改成`1.0.1`,在主题文件夹类就会生成`1.0`、`1.0.1`两个文件夹 72 | 73 | > 当然你可以忽略它,使用`git`的进行分支、版本管理 74 | 75 | ## 缺点 76 | 77 | 致命缺点:对于一个博客站点而言,使用服务器端渲染的方案,对`SEO`是友好的,`SPA`方案应当受到摒弃,前提是你的博客文章非常优质,那么我并不推荐你使用`Yuna`进行主题开发。 78 | 79 | 但是从项目层面去考虑,作为一个小型的程序,`SPA`的体验非常优秀,并且使用`vue.js`取代`jQuery`不香吗?清晰的、可维护、易迭代的代码不香吗? 80 | 81 | ## 额外说明 82 | 83 | - 图片压缩:脚手架默认使用了`Base64`压缩图片的方案 84 | 85 | - 网站图标:`/public/favicon.ico`是由`HtmlWebpackPlugin`插件模板引擎自动注入到`index.php`中,所以如果你需要修改默认的`favicon.ico`,请替换你喜欢的`ico`文件 86 | 87 | - 主题图片:在主题文件夹中,放入`screenshot.png` 88 | --------------------------------------------------------------------------------