npm 包抽离
经过前面步骤的开发后,到了 npm 包抽离阶段,首先的工作是把原本在 elpis 中便于功能验证和调试所留下的业务代码。
启动文件
js
// 引入elpis-core
const ElpisCore = require('./elpis-core');
// 启动项目
ElpisCore.start({
name:'elpis',
homePage: '/view/project-list'
});
这是原本在 elpis 中我们的启动文件,只需要考虑运行我们的 elpis 时引入 elpis-core,但是既然作为 npm 包提供能力出去,显然是不够的,需要把启动项目作为方法暴露出去给用户在自己的工程中调用,所以需要改造一下启动文件。
js
// index.js
module.exports = {
/**
* 启动 elpis
* @param {*} options 项目配置,透传到 elpis-core
* @returns app 实例
*/
serverStart(options = {}) {
const app = ElpisCore.start(options);
return app;
}
}
经过改造之后,用户在自己的项目启动文件中,引入 serverStart 之后并调用,就可以启动 elpis。
服务端
服务端需要处理的首先就是 elpis-core 中的逻辑,前面讲到 elpis-core 中通过各种 loader 扫描目录和加载对应目录中的文件,把 controller、 service、 extend、 router 等等挂载到 app 实例中。那么现在的问题是,之前 elpis 作为一个项目运行,我们通过以下方式读取文件,而定义好的业务路径 app.businessPath 也就是 app.businessPath = path.resolve(process.cwd(), .${sep}app); 只向的是运行目录下的 app 目录,那么一来当用户在自己的工程目录下并且使用我们的 npm 包时,指向的就是工程目录下的 app 目录,原本 elpis 中我们内置的一些功能就没有被加载和挂在到 app 上。
js
// 读取app/controller/**/**.js 下所有的文件
const controllerPath = path.resolve(app.businessPath, `.${sep}controller`);
const fileList = glob.sync(path.resolve(controllerPath, `.${sep}**${sep}**.js`));
所以首先需要对我们的 loader 都进行改造,思路就是分两部分进行加载,一是 elpis 自带的文件,二是业务下的文件。以 controller 的 loader 为例,有关 elpis 内置能力的文件目录指向要用 __dirname 替换掉原本的 process.cwd() 以及路径做一些相应的更改使得内置的文件能够正确加载进来,以及把处理文件挂载部份的代码逻辑抽成函数,方便这两部份文件处理。
js
// 读取 elpis/app/controller/**/**.js 下所有的文件
const elpisControllerPath = path.resolve(__dirname, `..${sep}..${sep}app${sep}controller`);
const elpisFileList = glob.sync(path.resolve(elpisControllerPath, `.${sep}**${sep}**.js`));
elpisFileList.forEach(file => {
handleFile(file);
})
// 读取 业务/app/controller/**/**.js 下所有的文件
const businessControllerPath = path.resolve(app.businessPath, `.${sep}controller`);
const businessFileList = glob.sync(path.resolve(businessControllerPath, `.${sep}**${sep}**.js`));
businessFileList.forEach(file => {
handleFile(file);
})
其余几个 loader 的处理思路基本和 controller loader 的处理思路大差不差,都需要对两部分文件的路径进行处理和分别加载,需要注意的是 elpis-core 的启动文件 index.js 中也需要对全局中间件进行处理。
处理完这些还没结束,像 elpis 中提供的 Controller、Service 的基类,我们也得暴露出去提供给用户使用,所以入口文件中还需要把这部分也给加进去。
js
//index.js
module.exports = {
/**
* 服务端基础
*/
Controller: {
Base: require('./app/controller/base.js')
},
Service: {
Base: require('./app/service/base.js')
},
....
}
自此服务端方面的抽离、改造也基本结束
前端工程化
webpack 配置方面需要改造的地方就不少了,首先要做的是把 elpis 中的 webpack 启动文件dev.js 和 prod.js 改造成方法暴露出去,然后在 elpis 的启动文件中把这两个方法暴露出去。
js
// index.js
// 引入前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev.js');
const FEBuildProd = require('./app/webpack/prod.js');
module.exports = {
...
/**
* 编译构建前端工程化
* @param {*} env 环境变量 dev/prod
*/
frontendBuild(env) {
if(env === 'local') {
FEBuildDev();
} else if(env === 'production') {
FEBuildProd();
}
},
}
这么一来在用户侧则可以自定义自己的工程化启动文件中,通过不同环境启动对应的 elpis 工程化。
js
// build.js
const {
frontendBuild
} = require('@woon_san/elpis');
// 编译构建前端产物
frontendBuild(process.env.NODE_ENV)
接下来需要做的是把用户工程里的潜在 webpack 配置拿过来跟我们的配置合并。
js
// 加载 业务 webpack 配置文件
let businessWebpackConfig = {};
try{
businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`);
} catch(e) {
console.error('Failed to load business webpack config:', e);
}
module.exports = merge.smart({ elpis webpack 配置...}, businessWebpackConfig)
接下来需要处理的是入口配置,因为是多页面应用,所以在原有配置中我们已经有动态构造 pageEntries 的逻辑了,只不过现在不仅需要构造 elpis 中的一些页面,还需要把用户工程目录中的页面入口也动态构造出来,思路跟前面也是差不多的,分 elpis 和 business 两部分构造,分别获取对应目录下的所有入口文件 entry.x.js 结合 HtmlWebpackPlugin 处理,最终得到以下配置。
js
// 入口配置
entry: Object.assign({}, elpisPageEntries, businessPageEntries),
...
// 构造最终渲染的页面模板
...elpisHtmlWebpackPluginList,
...businessHtmlWebpackPluginList
处理完入口配置,在用户侧启动工程化时会遇到卡点,会报一堆类似以下的错误
js
Cannot find package '@babel/plugin-transform-runtime' imported from /Users/woonsan/development/elpis-demo/babel-virtual-resolve-base.js
原因则是我们的 webpack 中用到很多 loader 比如 babel-loader、url-loader 等等,默认使用构建时的 cwd 也就是我们的用户工程目录,但是用户工程中是大概率没有安装这些包的,所以我们需要把用到 loader 的地方改造成 require.resolve('xxx-loader') 固定从 Elpis 加载。
接下来则是把一些公用的组件、工具函数、状态管理等等加上 elpis 前缀以 alias 别名的方式暴露出去。在处理这些别名的过程中,遇到相对卡住的地方,像用户自定义页面的话也许会定义一个 router.js 或者 route.js 之类的路由拓展配置,我们需要把这部份路由配置给整合到页面路由中,如果我们只是生硬的定义一个别名并且从用户侧去取,webpack 在静态构建时取不到正确文件时候则会报错。
js
alias: {
...
// 有可能会报错
$businessDashboardRouterConfig: path.resolve(process.cwd(), './app/pages/dashboard/router.js')
}
因此 alias 这部分也需要改造一下, 在 webpack 配置目录下的 libs 目录中建一个空白导出的文件, 通过先判断路由文件是否存在,不存在则以这个到处空白对象的文件替换,保证构建不报错。
js
alias: (() => {
const aliasMap = {};
const blankModulePath = path.resolve(__dirname, '../libs/blank.js');
// dashaboard 路由拓展配置
const businessDashboardRouterConfigPath = path.resolve(process.cwd(), './app/pages/dashboard/router.js');
aliasMap['$businessDashboardRouterConfig'] = fs.existsSync(businessDashboardRouterConfigPath) ? businessDashboardRouterConfigPath : blankModulePath;
return {
...,
...aliasMap
}
})()
同理我们还需要处理 schema-view component 组件拓展配置、widgets schema-form form-item 组件拓展配置、wigets schema-search-bar search-item 组件拓展配置。这么一来工程化这部分的改造也基本完成。
npm 发布
在发布之前,还需要把开发依赖 devDependencies 中的部分依赖迁移到 dependencies 中去,只留下部分必要的依赖。 npm 发布的流程就不再赘述了,需要注意的是写好详细的文档,指引使用这个包的用户如何使用。