1. 搭建项目文件
后端项目文件夹如下:
项目目录assets --静态资源
config --项目配置项
controllers --路由
models --java等后台请求,模版
tests --测试
views --前端视图
app.js --项目启动文件
logs --日志
middleawares --容错处理
utils --存放一些公用的函数等
总项目文件如下:
image
dist --项目打包后的文件
docs --文档接口文件
gulpfile.js --gulp配置
config --webpack配置文件
scripts --shell命令文件
src --项目源文件
二、后端架构
- 首先配置app.js
import Koa from "koa";
import serve from 'koa-static'; // 静态文件引入
import config from "./config"; // 配置文件
import render from 'koa-swig'; // 采用koa模版
import co from 'co';
import log4js from 'log4js';
import errorHandler from "./middleawares/errorHandler"; // 容错机制
const app = new Koa();
//前端模板
//co的作用是把 *函数全部自动向下执行 next -> next -> done
//async await 语法糖版本 koa-swig 并为KOA2 升级 KOA1
app.context.render = co.wrap(render({
root: config.viewDir,
autoescape: true,
cache: false, // cache: 'memory', // disable, set to false 模版缓存,开发时设为false,上线时再这样设置
varControls: ["[[", "]]"], // 换为[[]],防止和vue冲突
ext: 'html',
writeBody: false
}));
log4js.configure({ // log4配置
appenders: {
cheese: {
type: 'file',
filename: './logs/yd-log.log' // 日志路径
}
},
categories: {
default: {
appenders: ['cheese'],
level: 'error'
}
}
});
const logger = log4js.getLogger('cheese');
errorHandler.error(app, logger); // 若错处理
require("./controllers")(app); // 引入路由
//配置静态资源
app.use(serve(config.staticDir)); // 引入静态文件
app.listen(config.port, () => {
console.log("服务已启动🍺");
});
- 设置config文件
import {
join
} from "path";
import _ from "lodash";
console.log("取到的环境变量", process.env.NODE_ENV);
let config = {
"viewDir": join(__dirname, "..", "views"), // 前端页面
"staticDir": join(__dirname, "..", "assets") // 静态资源目录
}
if (process.env.NODE_ENV == "development") {
const localConfig = {
baseUrl: "http://localhost/index.php?r=", // 后端请求url
port: 8080
}
config = _.extend(config, localConfig);
}
//留给大家 PM2启动
if (process.env.NODE_ENV == "production") {
const prodConfig = {
port: 8081
}
config = _.extend(config, prodConfig);
}
module.exports = config;
- 配置容错处理
const errorHandler = {
error(app,logger){
app.use(async(ctx,next)=>{ // 服务器内部出错情况
try{
await next();
}catch(error){ // 或者统一返回出错页面
logger.error(error);
ctx.status = error.status || 500;
ctx.body = error;
}
});
app.use(async(ctx,next)=>{ // 404返回统一的404页面
await next();
if(404 != ctx.status){
return;
}
//百度K权
//ctx.status = 200;
ctx.status = 404;
ctx.body = '<script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8" homePageUrl="/" homePageName="回到我的主页"></script>';
})
}
}
module.exports = errorHandler;
- 路由设置
const IndexController = require("./IndexController");
const BooksController = require("./BooksController");
const indexControll = new IndexController();
const bookControll = new BooksController();
const router = require('koa-simple-router')
// const init = (app) => {
// }
//初始化所有的路由
module.exports = (app)=>{
app.use(router(_ => {
_.get('/', indexControll.actionIndex())
// index.php?r=index/data
_.get('/book', bookControll.actionIndex());
_.get('/book/savedata', bookControll.actionSaveData());
_.get('/book/add', bookControll.actionCreate());
}));
}
- model设置
/**
* @fileOverview 实现Index数据模型
* @author yuanzhijia@yidengxuetang.com
*/
const SafeRequest = require("../utils/SafeRequest");
/**
* Index类 获取后台有关于图书相关数据类
* @class
*/
class Index{
/**
* @constructor
* @param {string} app KOA2执行的上下文环境
*/
constructor(app){}
/**
* 获取后台的全部图书数据方法
* @param {*} options 配置项
* @example
* return new Promise
* getData(url, options)
*/
getData(options){
const safeRequest = new SafeRequest("book");
return safeRequest.fetch({});
}
/**
* 获取后台的全部图书数据方法
* @param {*} options 配置项
* @example
* return new Promise
* saveData(url, options)
*/
saveData(options){
const safeRequest = new SafeRequest("books/create");
return safeRequest.fetch({
method: 'POST',
params:options.params
});
}
}
module.exports = Index;
- 接下来我们来封装公共请求SafeRequest
const fetch = require('node-fetch');
const config = require("../config");
//统一代码 处理容错
class SafeRequest {
constructor(url) {
this.url = url;
this.baseURL = config.baseUrl;
}
fetch(options) {
return new Promise((resolve, reject) => {
//失败
let result = {
code: 0, //正常请求状态
message: "",
data: []
};
let yfetch = fetch(this.baseURL + this.url);
if (options.params) {
yfetch = fetch(this.baseURL + this.url, {
method: options.method,
body:options.params
})
}
yfetch
.then(res => res.json())
.then((json) => {
result.data = json;
resolve(result);
}).catch((error) => {
result.code = 1;
//mail 服务器 直接打电话 发邮件
result.message = "node-fetch请求失败,后端报警!";
reject(result)
})
})
}
}
module.exports = SafeRequest;
- 多页转单页
以一个列表页为例:
const Index = require("../models/Index");
const {
URLSearchParams // 获取参数
} = require('url');
const cheerio = require('cheerio')
class IndexController {
constructor() {
}
actionIndex() {
return async (ctx, next) => {
// ctx.body = 'hello world'
//Header由第一次ctx.body设置的
//输出的内容已最后一次
const index = new Index();
const result = await index.getData();
// console.log("整个node系统是否通了", result);
const html = await ctx.render("books/pages/index", {
data: result.data
});
if (ctx.request.header["x-pjax"]) {
const $ = cheerio.load(html);
//我只需要一小段html 基础的核心原理
ctx.body = $("#js-hooks-data").html();
// ctx.body = {
// html:$("#js-hooks-data").html(),
// css: <!-- injectcss -->,
// js: <!-- injectjs -->
// }
//CSR方式
//ctx.body = "<x-add></x-add>"
} else {
ctx.body = html;
}
}
}
actionCreate() {
return async (ctx, next) => {
const html = await ctx.render("books/pages/add");
if (ctx.request.header["x-pjax"]) {
const $ = cheerio.load(html);
let _result = "";
$(".pjaxcontent").each(function() {
_result += $(this).html();
});
console.log("_result", _result)
$(".lazyload-css").each(function() {
_result += $(this).html()
});
$(".lazyload-js").each(function() {
// _result += `<script src="${$(this).attr("src")}"></script>`
_result = `<script>${$(this).attr("src")}</script>`
});
ctx.body = _result;
} else {
ctx.body = html;
}
}
}
actionSaveData() {
return async (ctx, next) => {
const index = new Index();
const params = new URLSearchParams();
params.append("Books[title]", "测试的书名");
params.append("Books[author]", "测试作者");
params.append("Books[publisher]", "测试出版社");
const result = await index.saveData({
params
});
ctx.body = result;
}
}
}
module.exports = IndexController;
多页转单页,同时我们需要设置
$(document).pjax("a", "#app");
这样就可以根据请求头判断请求
- 模版页面
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="/styles/index.css">
{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>
三、前端架构
- 来使用下xtag
// add.html
div class="add pjaxcontent">
<x-add></x-add>
</div>
// add.js
const add = {
init(){
xtag.create('x-add', class extends XTagElement {
constructor() {
super();
this.datas = {
}
}
'::template(true)' (){
return `<h3>添加新闻</h3>
<form>
<div class="form-group">
<label for="exampleInputEmail1">书名</label>
<input type="text" class="form-control" id="exampleInputText1" placeholder="请输入书名">
</div>
<div class="form-group">
<label for="exampleInputEmail1">作者</label>
<input type="text" class="form-control" id="exampleInputText2" placeholder="请输入作者">
</div>
<div class="form-group">
<label for="exampleInputEmail1">出版社</label>
<input type="text" class="form-control" id="exampleInputText3" placeholder="请输入出版社">
</div>
<button id="add-btn" type="button" class="btn btn-info">提交</button>
</form>`
}
"click::event"(e){
if(e.target.id == "add-btn") {
alert("qq")
}
}
});
}
}
export default add;
- 在组件中使用js,css
// banner.html
<div class="components-banner">
<div class="container">
<div class="nav">
<a href="/book/">新闻列表</a>
<a href="/book/add">添加新闻</a>
</div>
</div>
</div>
// banner.css
:root{
--heightUnit: 70px;
}
.components-banner{
height: var(--heightUnit);
line-height: var(--heightUnit);
margin-bottom: 10px;
box-shadow: 0 0 10px gray;
& .nav{
color: purple;
}
}
// banner.js
import("./banner.css");
const banner = {
init(){
console.log("banner");
}
}
export default banner;
在页面中引入以上组件
{% extends 'common:layout.html' %}
{% block title %} 新闻列表 {% endblock %}
{% block styles %}
<!-- injectcss --> // 占位符
{% endblock %}
{% block header %}
{% include "components:banner/banner.html"%}
{% endblock %}
{% block content %}
{% include "components:list/list.html"%}
{% endblock %}
{% block scripts %}
<!-- injectjs -->
{% endblock %}
页面启动文件
// books-index.entry.js
import banner from "../../components/banner/banner.js";
import list from "../../components/list/list.js";
list.init();
banner.init();
四、对前后端项目打包
- 前端项目打包
const merge = require("webpack-merge"); // 合并插件
const argv = require("yargs-parser")(process.argv.slice(2)); // 获取当前环境
const _mode = argv.mode || "development";
const _modeflag = (_mode == "production" ? true : false);
const _mergeConfig = require(`./config/webpack.${_mode}.js`); // 引入开发/线上环境配置
const glob = require("glob"); // node的glob模块允许你使用 *等符号, 来写一个glob规则
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin'); // 刷新页面
const HtmlAfterWebpackPlugin = require('./config/HtmlAfterWebpackPlugin');
const ManifestPlugin = require("webpack-manifest-plugin"); // 生成映射文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {
join
} = require("path");
//需要处理的入口文件
let _entry = {};
//插件系统
let _plugins = [];
const files = glob.sync("./src/webapp/views/**/*.entry.js"); // 寻找入口js
for (let item of files) {
if (/.+\/([a-zA-Z]+-[a-zA-Z]+)(\.entry\.js$)/g.test(item) == true) {
const entryKey = RegExp.$1;
_entry[entryKey] = item;
const [dist, template] = entryKey.split("-");
_plugins.push(new HtmlWebpackPlugin({
filename: `../views/${dist}/pages/${template}.html`,
template: `src/webapp/views/${dist}/pages/${template}.html`,
inject: false,
chunks: ["runtime", "commons", entryKey], // 页面注入chunks
minify: {
collapseWhitespace: _modeflag, // 去空格
// removeComments: _modeflag // 注释不能删
}
}))
}
}
let cssLoaders = [MiniCssExtractPlugin.loader, {
loader: "css-loader"
},{
loader: "postcss-loader"
}
]
// 开发环境
!_modeflag && cssLoaders.unshift("css-hot-loader")
const webpackConfig = {
module: {
rules: [{
test: /\.css$/,
use: cssLoaders
}]
},
entry: _entry,
plugins: _plugins,
// watch: !_modeflag,
output: {
path: join(__dirname, "./dist/assets"),
publicPath: "/",
filename: "scripts/[name].bundule.js"
},
optimization: {
splitChunks:{
cacheGroups:{
commons:{
chunks:"initial",
name:"commons",
minChunks:3,
minSize:0
}
}
},
runtimeChunk:{
name: "runtime"
}
},
plugins: [
..._plugins,
new MiniCssExtractPlugin({
filename: _modeflag ? "styles/[name].[contenthash:5].css" : "styles/[name].css",
chunkFilename: _modeflag ? "styles/[name].[contenthash:5].css" : "styles/[name].css"
}),
new HtmlAfterWebpackPlugin(),
new ManifestPlugin(),
new LiveReloadPlugin()
]
};
module.exports = merge(webpackConfig, _mergeConfig);
// HtmlAfterWebpackPlugin
const pluginName = 'HtmlAfterWebpackPlugin';
const assetsHelp = (data) => {
let css = [];
let js = [];
const dir = {
js: item => `<script class="lazyload-js" type="text/javascript" src="${item}"></script>`,
css: item => `<link class="lazyload-css" rel="stylesheet" type="text/css" href="${item}">`
}
for (let jsitem of data.js) {
js.push(dir.js(jsitem))
}
for (let cssitem of data.css) {
css.push(dir.css(cssitem))
}
return {
js,
css
}
}
class HtmlAfterWebpackPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, compilation => {
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap(pluginName, htmlPluginData => { // htmlWebpackPluginAfterHtmlProcessing webpack4钩子
let _html = htmlPluginData.html;
_html = _html.replace(/components:/g, '../../../components/'); // 替换components:
_html = _html.replace(/common:/g, '../../common/');
const result = assetsHelp(htmlPluginData.assets);
_html = _html.replace("<!-- injectcss -->", result.css.join(""));
_html = _html.replace("<!-- injectjs -->", result.js.join(""));
htmlPluginData.html = _html;
})
});
}
}
module.exports = HtmlAfterWebpackPlugin;
// webpack.development.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
const {
join
} = require("path");
module.exports = {
plugins: [
new CopyWebpackPlugin([{
from: join(__dirname, "../", "/src/webapp/views/common/layout.html"),
to: '../views/common/layout.html'
}]),
new CopyWebpackPlugin([{
from: join(__dirname, "../", "/src/webapp/components"),
to: '../components'
}], {
copyUnmodified: true,
ignore: ['*.js', '*.css']
})
]
}
// webpack.production.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
const {
join
} = require("path");
const minify = require("html-minifier").minify; // 压缩html/css/js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); // 压缩css
// shash chunkhash contenthash
module.exports = {
output: {
filename: "scripts/[name].[contenthash:5].bundule.js"
},
plugins: [
new OptimizeCssAssetsPlugin({ // 压缩css
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true
}),
new CopyWebpackPlugin([{
from: join(__dirname, "../", "/src/webapp/views/common/layout.html"),
to: '../views/common/layout.html'
}]),
new CopyWebpackPlugin([{
from: join(__dirname, "../", "/src/webapp/components"),
to: '../components',
transform(content){
//html hint 优化
return minify(content.toString("utf-8"), { // 压缩html
collapseWhitespace: true // 去空格
});
}
}], {
ignore: ['*.js', '*.css']
})
]
}
- 后端项目打包
将node中的require改为import写法
//1.简单 快
//2.webpack 前端打包工具
const gulp = require("gulp");
const babel = require("gulp-babel");
const watch = require('gulp-watch'); // 监听文件改变
const rollup = require('gulp-rollup');
const replace = require('rollup-plugin-replace');
const entry = 'src/nodeuii/**/*.js';
//并行工具gulp-sequence
//开发环境
function builddev() {
return watch(entry, {
ignoreInitial: false
}, function () {
gulp.src(entry)
.pipe(babel({
babelrc: false,
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }],
"transform-es2015-modules-commonjs"
]
}))
.pipe(gulp.dest('dist'))
});
}
// //上线环境
// gulp.task("buildprod", () => {
function buildprod() {
return gulp.src(entry)
.pipe(babel({
babelrc: false,
ignore: ["./src/nodeuii/config/*.js"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }],
"transform-es2015-modules-commonjs"
]
}))
.pipe(gulp.dest('dist'));
}
function buildconfig() { // 流清洗
return gulp.src(entry)
.pipe(rollup({
output: {
format: "cjs"
},
input: "./src/nodeuii/config/index.js",
plugins: [
replace({ // 删除死代码
"process.env.NODE_ENV": JSON.stringify('production')
})
]
}))
.pipe(gulp.dest('./dist'));
}
let build = gulp.series(builddev); // gulp.series 串行执行
if (process.env.NODE_ENV == "production") {
//这里出现了问题
build = gulp.series(buildprod,buildconfig);
}
if (process.env.NODE_ENV == "lint") {
build = gulp.series(buildlint);
}
gulp.task("default", build);
网友评论