prev: Elpis-动态组件设计
接上一篇文章,Elpis项目已经基本完成了。但是为了更方便使用,可以把项目进行改造,发布到 npm 上面。包名叫 @qsb-elpis/elpis
。
优点:
- 别人可以通过安装 npm 包方式使用,方便创建项目,而不是去仓库下载。
- 内部的一些源码别人改不了,避免一些新人随意篡改核心的代码。
- 后续可以通过 npm 升级,使用方可以更容易同步升级。
缺点就不说了。
既然要变成通用的 npm 包 @qsb-elpis/elpis
,那么之前写的业务代码就必须清理掉,把项目挖空。业务代码由使用方通过在自己项目安装 @qsb-elpis/elpis
后,按照约定的方式开发。
大致目录结构如下:
为方便称呼,我们把要改造成 @qsb-elpis/elpis
的本地项目叫 elpis-base
。
elpis-base 的根目录的index.js,不能直接执行启动函数,而是把启动函数暴露给外部,由外部执行。
// 引入 elpis 核心
const Elpis = require("./elpis-core");
// 引入 前端工程构建方法
const FEBuildDev = require("./app/webpack/dev.js");
const FEBuildProd = require("./app/webpack/prod.js");
// TODO: 临时做个标记,证明是本地的,调试完后删掉
console.log("\x1b[31m", "当前正在使用本地 elpis-base 代码", "\x1b[0m");
module.exports = {
// 控制器与服务
Controller: {
Base: require("./app/controller/base.js"),
},
Service: {
Base: require("./app/service/base.js"),
},
// 打包构建
frontendBuild(env) {
if (env === "local") {
FEBuildDev();
} else if (env === "production") {
FEBuildProd();
}
},
serverStart(options = {}) {
const app = Elpis.start(options);
return app;
},
};
同理 app/webpack/dev.js
、app/webpack/prod.js
也要封装成函数,导出给外部使用。以prod.js为例子:
const webpack = require("webpack");
const webpackProdConfig = require("./config/webpack.prod.js");
module.exports = () => {
console.log("\n building... \n");
webpack(webpackProdConfig, (err, stats) => {
if (err) {
console.log("err ", err);
return;
}
process.stdout.write(
stats.toString({
colors: true, // 使用颜色标识
modules: false, // 不显示每个模块的打包信息
children: false, // 不显示子编译任务信息
chunks: false, // 不显示每个代码块的信息
chunkModules: true, // 显示代码块中模块的信息
}) + "\n\n"
);
});
};
elpis-base
的 package.json
的 name 要改成 @qsb-elpis/elpis
。
同时为了方便边看效果边开发调试,新建一个示例项目 elpis-demo
进行观察和实践,elpis-demo
目录 和 elpis-base
同一层级。
# 初始化 elpis-demo
npm init -y
package.json 依赖本地的 "@qsb-elpis/elpis": "file:../elpis-base"
{
"name": "elpis-demo",
"version": "1.0.0",
"description": "elpis-demo",
"main": "server.js",
"devDependencies": {
"nodemon": "^3.1.9"
},
"scripts": {
"dev": "_ENV='local' nodemon ./server.js",
"beta": "_ENV='beta' PORT=8081 node ./server.js",
"prod": "_ENV='production' node ./server.js",
"build:dev": "_ENV='local' node --max-old-space-size=4096 ./build.js",
"build:prod": "_ENV='production' node ./build.js"
},
"keywords": [],
"author": "qsb3008",
"license": "ISC",
"dependencies": {
"@qsb-elpis/elpis": "file:../elpis-base"
}
}
新建 server.js
作为启动文件
const { serverStart } = require("@qsb-elpis/elpis");
// 启动 elpis 服务
const app = serverStart({
name: "ElpisDemo",
icon: "/static/logo.png",
homePage: "/view/project-list",
});
新建 build.js
作为构建脚本文件。
const { frontendBuild } = require("@qsb-elpis/elpis");
// 编译构建前端工程
frontendBuild(process.env._ENV);
npm install
安装依赖。
npm run build:dev
编译构建前端工程
npm run dev
运行项目。
所以上面的一系列操作,其实是把原本在 elpis-base
做的打包和运行服务的操作,挪到了 elpis-demo
。
elpis-demo 项目能够使用本地的 elpis-base
,并且正常运行,再往后进行操作。
接下来的工作大都是细节方面的处理,比较繁琐。
由于elpis-base
是暴露给外部用的,所以很多的插件不再是开发依赖,要挪到dependencies
依赖中。除了以下是devDependencies
,其余依赖全部移动到dependencies
。
"devDependencies": {
"assert": "^2.0.0",
"babel-eslint": "^10.0.2",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-vue": "^9.17.0",
"ghooks": "~1.0.3",
"mocha": "^6.1.4",
"supertest": "^4.0.2",
"validate-commit-msg": "~2.14.0"
}
为了让使用方 elpis-demo
,能扩展自定义内容,elpis-base
的 elpis-core/loader 必须进行修改,把自身的app
目录下的controller、config、router等配置和elpis-demo
app目录下的配置合并到一起。
以 elpis-core/loader/router.js
为例,修改内容如下。
// 找到 elpis 路由文件
const elpisRouterPath = path.resolve(
__dirname,
`..${sep}..${sep}app${sep}router`
);
// 注册所有 elpis 路由
const elpisFileList = glob.sync(
path.resolve(elpisRouterPath, `.${sep}**${sep}**.js`)
);
elpisFileList.forEach((file) => {
// router file 导出一个函数,参数为 app 和 router
// 执行函数时,会将路由注册到koa-router上
require(path.resolve(file))(app, router);
});
// 找到 业务 路由文件
const businessRouterPath = path.resolve(app.bussinessPath, `.${sep}router`);
// 注册所有 业务 路由
const businessFileList = glob.sync(
path.resolve(businessRouterPath, `.${sep}**${sep}**.js`)
);
businessFileList.forEach((file) => {
// router file 导出一个函数,参数为 app 和 router
// 执行函数时,会将路由注册到koa-router上
require(path.resolve(file))(app, router);
});
处理的逻辑是把 elpis-base
和elpis-demo
下的 app/router
的文件都进行加载。以类似的手段处理以下文件:
- elpis-core/loader/config.js
- elpis-core/loader/controller.js
- elpis-core/loader/extend.js
- elpis-core/loader/middleware.js
- elpis-core/loader/router-schema.js
- elpis-core/loader/router.js
require.resolve
require.resolve
是 Node.js 中用于解析模块路径的内置函数,与require
不同,它仅返回模块的完整路径而不加载模块。- 相对路径解析时,
require.resolve
会从当前目录或默认node_modules
路径查找。
如图所示,大部分依赖都是写在elpis-base
, 而 elpis-demo
依赖了 elpis-base
(也就是发布后的@qsb3008-elpis/elpis
)。因此,webpack配置文件中,各种用到的loader,都要改为用require.resolve
包裹,才能真正找到对应的loader插件。
webpack.base.js 改造如下:
const glob = require("glob");
const path = require("path");
const fs = require("fs");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const merge = require("webpack-merge");
// 动态构造 elpis 页面入口和输出配置
const elpisPageEntries = {};
const elpisHtmlWebpackPluginList = [];
// 获取 elpis/app/pages 目录下所有入口文件 entry.[pageName].js
const elpisEntryList = path.resolve(__dirname, "../../pages/**/entry.**.js");
glob.sync(elpisEntryList).forEach((file) => handleFile(file, elpisPageEntries, elpisHtmlWebpackPluginList));
// 动态构造 业务 页面入口和输出配置
const businessPageEntries = {};
const businessHtmlWebpackPluginList = [];
// 获取 business/app/pages 目录下所有入口文件 entry.[pageName].js
const businessEntryList = path.resolve(process.cwd(), "./app/pages/**/entry.**.js");
glob.sync(businessEntryList).forEach((file) => handleFile(file, businessPageEntries, businessHtmlWebpackPluginList));
// 构造相关 webpack 处理数据结构
function handleFile(file, entrys = {}, pluginList = []) {
const entryName = path.basename(file, ".js");
// 生成webpack的入口entry配置
entrys[entryName] = file;
// 构造最终的渲染页面文件
pluginList.push(
new HtmlWebpackPlugin({
// 产物(最终模板)输出路径
filename: path.resolve(process.cwd(), "./app/public/dist/", `${entryName}.tpl`),
// 指定要使用的模板文件
template: path.resolve(__dirname, "../../view/entry.tpl"),
// 要注入的代码块
chunks: [entryName],
})
);
}
// 加载用户 自定义 业务webpack配置
let businessWebpackConfig = {};
try {
businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`);
} catch (error) {}
module.exports = merge.smart(
{
entry: Object.assign({}, elpisPageEntries, businessPageEntries),
output: {},
module: {
rules: [
{
test: /\.vue$/,
// 不能排除node_modules,eplis发布到npm,最终安装在node_modules,exclude导致编译失败
// exclude: /node_modules/,
use: { loader: require.resolve("vue-loader") },
},
{
test: /\.js$/,
include: [path.resolve(__dirname, "../../pages"), path.resolve(process.cwd(), "./app/pages")],
use: { loader: require.resolve("babel-loader") },
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: require.resolve("url-loader"),
options: {
limit: 1024,
esModule: false,
},
},
},
{
// 字体文件处理
test: /\.(woff|woff2|eot|ttf|otf)(\?\S*)?$/,
use: require.resolve("file-loader"),
},
],
},
// 配置模块解析的具体行为(定义webpack在打包时,如何找到并解析具体模块的路径)
resolve: {
extensions: [".js", ".vue", ".less", ".css"],
alias: (() => {
const aliasMap = {};
const blankModulePath = path.resolve(__dirname, "../libs/blank.js");
const businessDashboardRouterConfig = path.resolve(process.cwd(), "./app/pages/dashboard/router.js");
const isExist = fs.existsSync(businessDashboardRouterConfig);
aliasMap["$businessDashboardRouterConfig"] = isExist ? businessDashboardRouterConfig : blankModulePath;
// shcema-view Component 扩展配置字]\
const businessComponentConfig = path.resolve(
process.cwd(),
"./app/pages/dashboard/complex-view/schema-view/components/component-config.js"
);
aliasMap["$businessComponentConfig"] = fs.existsSync(businessComponentConfig)
? businessComponentConfig
: blankModulePath;
// shcema-form component 扩展配置
const businessFormItemConfig = path.resolve(
process.cwd(),
"./app/pages/widgets/schema-form/form-item-config.js"
);
aliasMap["$businessFormItemConfig"] = fs.existsSync(businessFormItemConfig)
? businessFormItemConfig
: blankModulePath;
// schema-Search-bar 扩展配置
const businessSearchItemConfig = path.resolve(
process.cwd(),
"./app/pages/widgets/schema-search-bar/search-item-config.js"
);
aliasMap["$businessSearchItemConfig"] = fs.existsSync(businessSearchItemConfig)
? businessSearchItemConfig
: blankModulePath;
const businessHeaderConfig = path.resolve(
process.cwd(),
"./app/pages/widgets/header-container/header-config.js"
);
aliasMap["$businessHeaderConfig"] = fs.existsSync(businessHeaderConfig)
? businessHeaderConfig
: blankModulePath;
return {
vue: require.resolve("vue"),
"@babel/runtime/helpers/asyncToGenerator": require.resolve("@babel/runtime/helpers/asyncToGenerator"),
"@babel/runtime/regenerator": require.resolve("@babel/runtime/regenerator"),
$elpisPages: path.resolve(__dirname, "../../pages"),
// 工具库
$elpisCommon: path.resolve(__dirname, "../../pages/common"),
$elpisCurl: path.resolve(__dirname, "../../pages/common/curl.js"),
$elpisUtils: path.resolve(__dirname, "../../pages/common/utils.js"),
// 元件
$elpisWidgets: path.resolve(__dirname, "../../pages/widgets"),
$elpisHeaderContainer: path.resolve(__dirname, "../../pages/widgets/header-container/header-container.vue"),
$elpisSiderContainer: path.resolve(__dirname, "../../pages/widgets/sider-container/sider-container.vue"),
$elpisSchemaTable: path.resolve(__dirname, "../../pages/widgets/schema-table/schema-table.vue"),
$elpisSchemaForm: path.resolve(__dirname, "../../pages/widgets/schema-form/schema-form.vue"),
$elpisSchemaSearchBar: path.resolve(__dirname, "../../pages/widgets/schema-search-bar/schema-search-bar.vue"),
// 缓存别名
$elpisStore: path.resolve(__dirname, "../../pages/store"),
// 启动文件 别名
$elpisBoot: path.resolve(__dirname, "../../pages/boot.js"),
// 扩展路由
...aliasMap,
};
})(),
},
// 配置 webpack 插件
plugins: [
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: "vue",
axios: "axios",
_: "lodash",
}),
// 定义全局变量
new webpack.DefinePlugin({
// 在 vue3 中,支持选项式api
__VUE_OPTIONS_API__: true,
//禁用生成环境 Vue 调试工具
__VUE_PROD_DEVTOOLS__: false,
// 禁用生产环境显示“水合”信息
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
}),
// 构造最终渲染的页面模板
...elpisHtmlWebpackPluginList,
...businessHtmlWebpackPluginList,
],
},
businessWebpackConfig
);
要加载和解析 elpis-demo
的内容,使用 path.resolve(process.cwd(), 路径)
查找。
要加载和解析 elpis-base
的内容,使用 path.resolve(__dirname, 路径)
查找。
同理,反正webpack各个环境的配置,按照以上的方式,根据实际需求进行处理,如 webpack.dev.js
、webpack.prod.js
文件等。
elpis-demo 添加一些数据进行测试,如果没问题,就可以把 elpis-base
发布到npm平台。
npm publish --access=public
目前npm包 @qsb3008-elpis/elpis
里面包含了一套内置的 dashboard
模版,提供了一些预置的组件,schema-form
、schema-table
等,以及相关的 input
、select
、inputNumber
等配套的元件。
我们可以使用内置的模板,配置出自己的项目,并且可以通过在app目录下继续添加各种组件、controller、service 等进行完善。也可以增加新的模板,当模板和组件使用的频率比较高的时候,我们可以继续沉淀到我们的npm包里面。
引用: 抖音“哲玄前端”《大前端全栈实践》